Skip to content

fix(expo): deduplicate native client startup requests#9140

Open
mikepitre wants to merge 1 commit into
mainfrom
mike/fix-expo-startup-requests
Open

fix(expo): deduplicate native client startup requests#9140
mikepitre wants to merge 1 commit into
mainfrom
mike/fix-expo-startup-requests

Conversation

@mikepitre

@mikepitre mikepitre commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Expo native-client startup could trigger repeated client refreshes across the JS and native runtimes. In the captured SDK 56 launch, that produced 11 Clerk requests, including eight identical GET /v1/client calls.

The duplicate requests came from native configuration racing JS client hydration, observer callbacks echoing bootstrap and JS-originated changes, and device-token updates being followed by a second explicit refresh. This change waits for the JS Clerk instance before reading its initial token, configures native once with that token, and reconciles the two clients after both sides are ready. Native observers now seed their initial snapshot and suppress bootstrap or JS-originated echoes without dropping later native changes.

Token-cache notifications are deduplicated, client changes that occur before native is ready are coalesced, and configure failures still release the synchronization barrier. Focused tests cover initial bootstrap, token rotation, remount recovery, configure failure, and native-to-JS reconciliation.

With the SDK 56 hosted-auth example unchanged, process-cold launches on iOS and Android each produce four Clerk requests: one JS and one native request for /v1/environment, plus one JS and one native request for /v1/client.

Summary by CodeRabbit

  • Improvements

    • Reduced redundant client refreshes during Expo startup and configuration.
    • Improved synchronization between native and JavaScript client state, including device-token updates and token changes.
    • Prevented duplicate client-change notifications during initialization and synchronization.
    • Improved recovery when clients or JavaScript runtimes reload.
  • New Features

    • Added an enabled option to control native client event subscriptions and synchronization.
    • Synchronization now waits until the Clerk client is ready, applying pending updates afterward.

@changeset-bot

changeset-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 245258e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 11, 2026 12:31pm
swingset Ready Ready Preview, Comment Jul 11, 2026 12:31pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Expo native and JavaScript client synchronization now gates startup on readiness, avoids redundant refreshes, suppresses duplicate client-change events, queues changes before readiness, and conditionally manages native event subscriptions. Android, iOS, provider wiring, hooks, tests, and release metadata are updated.

Changes

Expo client synchronization

Layer / File(s) Summary
Native token and refresh coordination
packages/expo/android/.../ClerkExpoModule.kt, packages/expo/ios/ClerkNativeBridge.swift
Native bridges normalize tokens, compare client state before refreshing, and suppress duplicate events during configuration and JavaScript-originated sync.
JavaScript bootstrap and queued synchronization
packages/expo/src/provider/nativeClientSync.tsx
Token-cache notifications now require actual value changes; bootstrap waits for Clerk readiness, reconciles cached and native tokens, and queues refreshes until synchronization is enabled.
Readiness-gated event subscriptions
packages/expo/src/hooks/useNativeClientEvents.ts, packages/expo/src/provider/ClerkProvider.tsx
Native listeners and client synchronization activate from isNativeClientReady and clean up when disabled.
Synchronization regression coverage and release metadata
packages/expo/src/hooks/__tests__/useNativeClientEvents.test.ts, packages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsx, .changeset/calm-clients-start.md
Tests cover listener lifecycle, readiness, queued changes, token suppression, recovery, and updated refresh behavior; a patch release is declared for @clerk/expo.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClerkProvider
  participant NativeClientSync
  participant ClerkInstance
  participant NativeModule
  ClerkProvider->>NativeClientSync: wait for native and Clerk readiness
  NativeClientSync->>ClerkInstance: read cached token and status
  NativeClientSync->>NativeModule: configure or synchronize token state
  NativeModule-->>NativeClientSync: client state and native events
  NativeClientSync->>ClerkInstance: apply queued refresh when enabled
Loading

Possibly related PRs

Poem

I’m a bunny guarding startup bright,
Fewer refreshes hop into sight.
Tokens queue till readiness gleams,
Duplicate events fade from dreams.
Native and JavaScript sync as one—
A tidy burrow, refreshes done!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: deduplicating Expo native client startup requests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9140

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9140

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9140

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9140

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9140

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9140

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9140

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9140

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9140

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9140

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9140

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9140

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9140

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9140

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9140

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9140

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9140

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9140

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9140

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9140

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9140

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9140

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9140

commit: 245258e

@mikepitre mikepitre changed the title [codex] fix(expo): deduplicate native client startup requests fix(expo): deduplicate native client startup requests Jul 11, 2026
@mikepitre mikepitre marked this pull request as ready for review July 16, 2026 02:25
@mikepitre mikepitre requested a review from a team July 16, 2026 02:25
@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-16T02:29:21.796Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on 245258e.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt (1)

403-436: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Handle cleared device tokens in the native sync path.
syncClientStateFromJs(..., null, ..., true) still leaves the previous native device token in place on both platforms, so clearing CLERK_CLIENT_JWT_KEY can repopulate stale client state instead of clearing it.

  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt#L403-L436: add an explicit clear branch when didChangeDeviceToken is true and deviceToken is null/blank.
  • packages/expo/ios/ClerkNativeBridge.swift#L317-L329: handle the nil token path the same way.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt`
around lines 403 - 436, Handle cleared device tokens in syncClientStateFromJs on
both platforms: in
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt lines
403-436, add an explicit branch for didChangeDeviceToken with a null or blank
deviceToken that clears the native device token; in
packages/expo/ios/ClerkNativeBridge.swift lines 317-329, apply the equivalent
nil-token clearing behavior. Ensure the existing non-empty token update path
remains unchanged.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/expo/src/provider/nativeClientSync.tsx`:
- Around line 814-826: Update the exported hook useNativeClientBootstrap with an
explicit named return interface describing isMountedRef and isNativeClientReady,
and annotate its return type accordingly. Also annotate the exported
useNativeClientEventSync hook with : void, preserving both hooks’ existing
behavior.

---

Outside diff comments:
In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt`:
- Around line 403-436: Handle cleared device tokens in syncClientStateFromJs on
both platforms: in
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt lines
403-436, add an explicit branch for didChangeDeviceToken with a null or blank
deviceToken that clears the native device token; in
packages/expo/ios/ClerkNativeBridge.swift lines 317-329, apply the equivalent
nil-token clearing behavior. Ensure the existing non-empty token update path
remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: b1301bea-1056-418a-bc97-fe47cecfdcff

📥 Commits

Reviewing files that changed from the base of the PR and between b51e224 and 245258e.

📒 Files selected for processing (8)
  • .changeset/calm-clients-start.md
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
  • packages/expo/ios/ClerkNativeBridge.swift
  • packages/expo/src/hooks/__tests__/useNativeClientEvents.test.ts
  • packages/expo/src/hooks/useNativeClientEvents.ts
  • packages/expo/src/provider/ClerkProvider.tsx
  • packages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsx
  • packages/expo/src/provider/nativeClientSync.tsx

Comment on lines 814 to 826
export function useNativeClientBootstrap({
publishableKey,
nativeRefreshFromJsControllerRef,
suppressTokenCacheNotificationsRef,
tokenCache,
clerkInstance,
}: {
publishableKey: string;
nativeRefreshFromJsControllerRef: MutableRefObject<NativeRefreshFromJsController | null>;
suppressTokenCacheNotificationsRef: MutableRefObject<number>;
tokenCache: TokenCache | undefined;
clerkInstance: SyncableClerkInstance | null | undefined;
}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant sections
wc -l packages/expo/src/provider/nativeClientSync.tsx
ast-grep outline packages/expo/src/provider/nativeClientSync.tsx --view expanded | sed -n '1,220p'
printf '\n--- lines 780-980 ---\n'
sed -n '780,980p' packages/expo/src/provider/nativeClientSync.tsx
printf '\n--- exported hook definitions ---\n'
rg -n "export function useNativeClient(Bootstrap|EventSync)|function useNativeClient(Bootstrap|EventSync)" packages/expo/src/provider/nativeClientSync.tsx

Repository: clerk/javascript

Length of output: 8748


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read only the relevant slices if the first command is not enough
sed -n '814,826p' packages/expo/src/provider/nativeClientSync.tsx
printf '\n---\n'
sed -n '944,960p' packages/expo/src/provider/nativeClientSync.tsx
printf '\n--- JSDoc/export context ---\n'
rg -n "useNativeClientBootstrap|useNativeClientEventSync|`@public`|`@since`|reference" packages/expo/src/provider/nativeClientSync.tsx

Repository: clerk/javascript

Length of output: 1325


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find nearby exported hooks in this file and similar files to compare return-type style
rg -n "export function use[A-Z].*\(" packages/expo/src packages -g '*.ts' -g '*.tsx' | sed -n '1,120p'

Repository: clerk/javascript

Length of output: 14928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- useNativeClientBootstrap body end ---'
sed -n '814,910p' packages/expo/src/provider/nativeClientSync.tsx
printf '\n%s\n' '--- useNativeClientEventSync body end ---'
sed -n '944,1001p' packages/expo/src/provider/nativeClientSync.tsx
printf '\n%s\n' '--- package export surface ---'
git ls-files packages/expo | rg '(^|/)index\.(ts|tsx|js|jsx|mjs|cjs)$|package\.json$'
printf '\n%s\n' '--- references to nativeClientSync.tsx ---'
rg -n "nativeClientSync\.tsx|useNativeClientBootstrap|useNativeClientEventSync|useSyncableTokenCache" packages/expo packages -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 8177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- provider barrel(s) ---'
git ls-files packages/expo/src/provider
printf '\n%s\n' '--- provider index / exports ---'
for f in packages/expo/src/provider/index.ts packages/expo/src/provider/index.tsx packages/expo/src/index.ts packages/expo/src/hooks/index.ts; do
  if [ -f "$f" ]; then
    echo "### $f"
    sed -n '1,220p' "$f"
    echo
  fi
done

printf '%s\n' '--- package entrypoints mentioning provider ---'
rg -n "provider/|ClerkProvider|useNativeClientBootstrap|useNativeClientEventSync|nativeClientSync" packages/expo/src/index.ts packages/expo/src/provider packages/expo/src/hooks -g '*.ts' -g '*.tsx'

Repository: clerk/javascript

Length of output: 9635


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether a result type already exists or is used nearby
rg -n "type .*Bootstrap|interface .*Bootstrap|UseNativeClientBootstrap|UseNativeClientEventSync|isNativeClientReady|isMountedRef" packages/expo/src/provider/nativeClientSync.tsx packages/expo/src/provider/ClerkProvider.tsx

Repository: clerk/javascript

Length of output: 1435


Add explicit return types to these exported hooks. useNativeClientBootstrap should use a named result interface for { isMountedRef, isNativeClientReady }, and useNativeClientEventSync should be annotated : void.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/expo/src/provider/nativeClientSync.tsx` around lines 814 - 826,
Update the exported hook useNativeClientBootstrap with an explicit named return
interface describing isMountedRef and isNativeClientReady, and annotate its
return type accordingly. Also annotate the exported useNativeClientEventSync
hook with : void, preserving both hooks’ existing behavior.

Sources: Coding guidelines, Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant