- Notifications
You must be signed in to change notification settings - Fork 419
feat(clerk-js,shared,react,nextjs): Migrate to useSyncExternalStore #7411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(clerk-js,shared,react,nextjs): Migrate to useSyncExternalStore #7411
Conversation
…uth instead of at the ClerkProvider level
… react package to use it
| user: this.user, | ||
| organization: this.organization, | ||
| }; | ||
| this.__internal_lastEmittedResources = resources; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clever!
…s-from-provider-and-rely-on
…s-from-provider-and-rely-on
…s-from-provider-and-rely-on
…s-from-provider-and-rely-on
With useSyncExternalStore, children components were re-rendering first, before this component had a chance to re-render and unmount them. This could cause situations where components that should already have been unmounted affected an unrelated page because their useEffect ran.
…s-from-provider-and-rely-on
…s-from-provider-and-rely-on
…pen before setActive finishes
Rename and move file from AuthContext to useAuthBase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of this is identical to the deleted AuthContext.ts, but git couldn't track it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
.changeset/full-parents-crash.md (1)
31-31: Minor grammar fix: use hyphen for compound adjective.When "signed out" functions as a compound adjective modifying "state", use a hyphen: "signed-out state" instead of "signed out state".
🔎 Apply this diff:
- * A bug where `useAuth` would sometimes briefly return the `initialState` rather than `undefined` - * This could in certain situations incorrectly lead to a brief `user: null` on the first page after signing in, indicating a signed out state + * A bug where `useAuth` would sometimes briefly return the `initialState` rather than `undefined` + * This could in certain situations incorrectly lead to a brief `user: null` on the first page after signing in, indicating a signed-out state
📜 Review details
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
.changeset/full-parents-crash.md(1 hunks)packages/shared/src/types/clerk.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/shared/src/types/clerk.ts
🧰 Additional context used
🪛 LanguageTool
.changeset/full-parents-crash.md
[grammar] ~31-~31: Use a hyphen to join words.
Context: ...ge after signing in, indicating a signed out state * Hydration mismatches in cert...
(QB_NEW_EN_HYPHEN)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
- GitHub Check: Integration Tests (nextjs, chrome, 15)
- GitHub Check: Integration Tests (nextjs, chrome, 16, RQ)
- GitHub Check: Integration Tests (quickstart, chrome, 16)
- GitHub Check: Integration Tests (quickstart, chrome, 15)
- GitHub Check: Integration Tests (billing, chrome, RQ)
- GitHub Check: Integration Tests (machine, chrome)
- GitHub Check: Integration Tests (nextjs, chrome, 16)
- GitHub Check: Integration Tests (nuxt, chrome)
- GitHub Check: Integration Tests (react-router, chrome)
- GitHub Check: Integration Tests (ap-flows, chrome)
- GitHub Check: Integration Tests (machine, chrome, RQ)
- GitHub Check: Integration Tests (billing, chrome)
- GitHub Check: Integration Tests (vue, chrome)
- GitHub Check: Integration Tests (custom, chrome)
- GitHub Check: Integration Tests (handshake:staging, chrome)
- GitHub Check: Integration Tests (astro, chrome)
- GitHub Check: Integration Tests (tanstack-react-start, chrome)
- GitHub Check: Integration Tests (handshake, chrome)
- GitHub Check: Integration Tests (sessions:staging, chrome)
- GitHub Check: Integration Tests (express, chrome)
- GitHub Check: Integration Tests (localhost, chrome)
- GitHub Check: Integration Tests (sessions, chrome)
- GitHub Check: Integration Tests (generic, chrome)
- GitHub Check: Formatting | Dedupe | Changeset
- GitHub Check: Build Packages
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (1)
.changeset/full-parents-crash.md (1)
1-32: Changelog entry accurately reflects PR scope.The changeset clearly documents the migration to
useSyncExternalStorewith appropriate version bumps, well-articulated breaking changes, clear migration guidance, new features, and bug fixes. Content aligns with PR objectives and the AI summary.
Description
This is the full implementation of PoCs #7194 and #7267.
The main goal of the PR is to refactor the SDKs away from using a top level
addListener+state+contextcombo for auth-state, to usinguseSyncExternalStore.ContextProvidersfromuiandreactintoshared/reactclerk.addListener()in that provider, also removes most contextsUserContextare no longer exported fromshared/reactclerkviauseSyncExternalStoreuseUserBaseare also reexported asuseUserContextto avoid breaking change for theseMore implementation notes
useClearQueriesOnSignOutas a conditional hook was causing the new tests to failinitialAuthStatefromuseAuthalso fixes a bug where the initial auth state was being used during the transitive statenullinstead ofundefinedon the page right after sign-in, which put the app in an akward state where it momentarily believed you were still signed out - Could lead to redirects back to/sign-inthis.#setAccessors();tothis.#updateAccessors();which now callsthis.#emit()by defaultskipInitialEmitto skip emittingclerkstate and the app state tearthis.#emit()this.__internal_lastEmittedResources, souseSEScan read the last resources emitted ingetSnapshotskipInitialEmitto avoid emitting and re-rendering onuseSES->subscribeclerk-jsWith all this done, I noticed two timing issues in the bridge between the host React app and the Components app, which is a separate app. Both happened because components that should have unmounted had time to re-render and fire effects, because
useSESemitted slightly earlier thansetState.ui/src/Components.tsx, the nodes (current components and which node they should be portaled into) was being set viasetStateSignInRouteshas a fallback, which is to redirect back to/sign-inTanStack Routerspecifically,SignInstayed mounted and re-rendered because ofuseSES, even though the node was gone and the next page had already been rendered<RedirectToSignIn />routenodesa ref insteaduseSyncExternalStorewhere we don't use the return value, to make sure<Components />re-renders before the child and unmounts itBaseRouter.tsx, the asyncbaseNavigatewas doingsetRoutePartsas part of the navigationuseSESin a child ran first and similar to the above case, triggered a redirect back to/sign-influshSyncto ensure the components had properly re-rendered, guaranteeingsetActivedoes not leave the transitive state before the host app AND the Clerk components are readyBoth of these fixes might be considered controversial, happy for feedback!
Note that the unmount flow has multiple
.then()which schedules unmounting into several microtasks even whenUIis available. We might want to make this synchronous for even tighter control, but this was not necessary to fix these bugs.Note also that these bugs result from having multiple React apps. The normal React lifecycle should guarantee they don't happen in host apps.
Background and motivation
Feel free to skip this if you already have familiarity, but as this is a fairly large change, I wanted to write down the full reasoning for this change for posterity.
As I see it, Clerk has different kinds of state. JWT state, piggybacking state and fetched state. Worth noting is that the fetched state has previously used SWR under the hood, but is about to change over to a custom React Query implementation, but both of these already uses
useSyncExternalStoreand are untouched by this PR.The JWT state is fairly self-explanatory, and the client piggybacking state is asynchronous state that is included as part of other API-calls as a self-refreshing mechanism. The
clerk-jsbase abstraction updates this for example when it polls for the/tokens, and pushes the changes to the SDKs.The JWT state and the piggybacking state together represents the foundational "auth state". This represents the user, the active organization etc.
Current solution
clerk-jshas anaddListenermethod. The React SDKs subscribes to this in an effect in their<ClerkProvider>and sets an internal state on every update. This state is then massaged a bit and placed on a few different contexts, and the different hooks, likeuseAuth,useUseretc read from these contexts.<ClerkProvider>also supports passing in aninitialState. If this is passed in, you can access it during SSR, but because there is no good way to prefetch the piggybacking or fetched state currently, only theuseAuthstate is really feasible to server render.In Next, doing
<ClerkProvider dynamic>automatically provides theinitialStateforuseAuthbehind the scenes. The reason we don't always do this is that it opts out of static generation and caching.Current challenges and issues
Subscribing at the top has a few downsides however. It's currently hard to make things more streamy by allowing
initialStateto be represented as a promise, as we currently need to await it at the top. This is something we want to do to supportcacheComponentsand Partial prerendering better, allowing for finer-grained caching. This also opens up for making things suspenseful.It also doesn't play well with concurrent rendering. A concrete example of this is an active bug that can happen when one subtree suspends long enough during page load that
clerk-jshas time to load. When it unsuspends, it reads the loaded auth state and not the initial one, causing hydration mismatches.Another downside is how context is hard to use for fine grained updates, making it hard to implement things like selectors on top of it.
useSyncExternalStoreis the canonical way to safely subscribe to an external store, and has none of these downsides.Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change
Summary by CodeRabbit
New Features
Refactor
Tests
✏️ Tip: You can customize this high-level summary in your review settings.