-
Notifications
You must be signed in to change notification settings - Fork 425
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
Open
Ephem
wants to merge
61
commits into
main
from
fredrik/user-4043-remove-clerk-state-contexts-from-provider-and-rely-on
Open
Changes from all commits
Commits
Show all changes
61 commits
Select commit
Hold shift + click to select a range
bc169ba
Refactor promises in NextJS ClerkProvider
Ephem 3fa0633
Remove PromisifiedAuth
Ephem e5d4d4b
Remove initialAuthState from useAuth
Ephem 3b6687e
Update expo useAuth to match new signature
Ephem 11a38da
Add changeset
Ephem cf02da1
Remove special isNext13 handling from Next ClerkProvider
Ephem 9f9477d
Temporarily remove affected packages from changeset
Ephem c7ed168
Add back affected packages
Ephem c4eadb4
Merge branch 'vincent-and-the-doctor' into fredrik/remove-initial-aut...
Ephem 2f45577
Fix changeset react package name
Ephem 09d8a8a
Add InitialAuthStateProvider and refactor to derive authState in useA...
Ephem 31d4f2b
Use InitialAuthStateProvider directly for nested Next ClerkProvider
Ephem 7a5381c
Resolve !dynamic without promises
Ephem a23789b
Merge branch 'vincent-and-the-doctor' into fredrik/remove-initial-aut...
Ephem ba26f9c
Clean up AuthContext
Ephem d72ec7c
Merge branch 'vincent-and-the-doctor' into fredrik/remove-initial-aut...
Ephem 19b996b
Merge branch 'vincent-and-the-doctor' into fredrik/remove-initial-aut...
Ephem a2adbf3
Remove AuthContext and add uSES for useAuth hook
Ephem c85a66c
Move ClerkContextProvider from ui to shared/react
Ephem f35bbbb
Update shared ClerkContextProvider to support initialState and switch...
Ephem 288cf94
Remove SessionContext and refactor to uSES
Ephem 5d87895
Remove UserContext and refactor to uSES
Ephem a8f9f60
Remove OrganizationProvider and refactor to uSES
Ephem f37d8d1
Remove ClientContext and refactor to uSES
Ephem d3d85d1
Support passing in initialState as a promise
Ephem 5b81912
Add skipInitialEmit option to addListener and use in uSES
Ephem c3c79f9
Remove unrelated changeset
Ephem 7b73924
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem 7581b74
Rename setAccessors -> updateAccessors and make it emit
Ephem 05debd5
Fix getSnapshot stale closure issue
Ephem 37eb323
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem e66fe43
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem 8f23b8b
Update tests
Ephem dfc6e64
Remove useDeferredValue
Ephem ba8373e
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem 185844f
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem f0b43d3
Introduce __internal_lastEmittedResources in clerk-js to enable prope...
Ephem 84fdb9b
Refactor initialState
Ephem 3244b9a
Merge branch 'main' into fredrik/poc
Ephem 9d6f7f9
Revert supporting initialState as a promise (for now)
Ephem e3b3d7e
Fix mergeNextClerkPropsWithEnv types
Ephem 1fed366
Trigger
Ephem c59b0bb
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
Ephem 7486150
Add basic transition tests
Ephem c371efe
Remove failing @ts-expect-error in test
Ephem b4af784
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
Ephem 565238b
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
Ephem 162e2ec
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
Ephem 4feac2c
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
Ephem 65d1344
Re-render Components controller when Clerk emits
Ephem e8efb64
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
Ephem 18db369
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
Ephem 9d30d64
Flush internal routing state synchronously to guarantee rerenders hap...
Ephem b0716b5
Add comment to flushSync
Ephem 1152cea
Add toBeSignedIn checks to transition tests
Ephem d12f639
Fix conditional hook in RQ implementation
Ephem 0c4909f
Rename useAuthState to useAuthBase to match other hooks
Ephem 5376088
Add skipInitialEmit to JSDoc
Ephem 598c13a
Add changeset
Ephem 586f2fc
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
jacekradko 8f52c75
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context...
jacekradko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
.changeset/full-parents-crash.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| --- | ||
| '@clerk/nextjs': major | ||
| '@clerk/shared': major | ||
| '@clerk/react': major | ||
| '@clerk/expo': major | ||
| '@clerk/chrome-extension': major | ||
| '@clerk/react-router': major | ||
| '@clerk/clerk-js': minor | ||
| '@clerk/tanstack-react-start': minor | ||
| --- | ||
|
|
||
| Refactor React SDK hooks to subscribe to auth state via `useSyncExternalStore`. This is a mostly internal refactor to unlock future improvements, but includes a few breaking changes and fixes. | ||
|
|
||
| Breaking changes: | ||
|
|
||
| * All `@clerk/react`-based packages: Removes ability to pass in `initialAuthState` to `useAuth` | ||
| * This was added for internal use and is no longer needed | ||
| * Instead pass in `initialState` to the `<ClerkProvider>`, or `dynamic` if using the Next package | ||
| * See your specific SDK documentation for more information on Server Rendering | ||
| * `@clerk/shared`: Removes now unused contexts `ClientContext`, `SessionContext`, `UserContext` and `OrganizationProvider` | ||
| * We do not anticipate public use of these | ||
| * If you were using any of these, file an issue to discuss a path forward as they are no longer available even internally | ||
|
|
||
| New features: | ||
|
|
||
| * `@clerk/clerk-js`: `addListener` now takes a `skipInitialEmit` option that can be used to avoid emitting immediately after subscribing | ||
|
|
||
| Fixes: | ||
|
|
||
| * 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 | ||
| * Hydration mismatches in certain rare scenarios where subtrees would suspend and hydrate only after `clerk-js` had loaded fully |
167 changes: 167 additions & 0 deletions
integration/templates/next-app-router/src/app/transitions/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| 'use client'; | ||
|
|
||
| import { OrganizationSwitcher, useAuth, useOrganizationList } from '@clerk/nextjs'; | ||
| import { OrganizationMembershipResource, SetActive } from '@clerk/shared/types'; | ||
| import { Suspense, useState, useTransition } from 'react'; | ||
|
|
||
| // Quick and dirty promise cache to enable Suspense "fetching" | ||
| const cachedPromises = new Map<string, Promise<unknown>>(); | ||
| const getCachedPromise = (key: string, value: string | undefined | null, delay: number = 1000) => { | ||
| if (cachedPromises.has(`${key}-${value}-${delay}`)) { | ||
| return cachedPromises.get(`${key}-${value}-${delay}`)!; | ||
| } | ||
| const promise = new Promise(resolve => { | ||
| setTimeout(() => { | ||
| const returnValue = `Fetched value: ${value}`; | ||
| (promise as any).status = 'fulfilled'; | ||
| (promise as any).value = returnValue; | ||
| resolve(returnValue); | ||
| }, delay); | ||
| }); | ||
| cachedPromises.set(`${key}-${value}-${delay}`, promise); | ||
| return promise; | ||
| }; | ||
|
|
||
| export default function TransitionsPage() { | ||
| return ( | ||
| <div style={{ margin: '40px' }}> | ||
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-between', | ||
| marginBottom: '60px', | ||
| alignItems: 'center', | ||
| }} | ||
| > | ||
| <TransitionController /> | ||
| <TransitionSwitcher /> | ||
| <div> | ||
| <div style={{ backgroundColor: 'white' }}> | ||
| <OrganizationSwitcher fallback={<div>Loading...</div>} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: '40px' }}> | ||
| <AuthStatePresenter /> | ||
| <Suspense fallback={<div data-testid='fetcher-fallback'>Loading...</div>}> | ||
| <Fetcher /> | ||
| </Suspense> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // This is a hack to be able to control the start and stop of a transition by using a promise | ||
| function TransitionController() { | ||
| const [transitionPromise, setTransitionPromise] = useState<Promise<unknown> | null>(null); | ||
| const [pending, startTransition] = useTransition(); | ||
| return ( | ||
| <div> | ||
| <button | ||
| onClick={() => { | ||
| if (pending) { | ||
| (transitionPromise as any).resolve(); | ||
| setTransitionPromise(null); | ||
| } else { | ||
| let resolve; | ||
| const promise = new Promise(r => { | ||
| resolve = r; | ||
| }); | ||
| // We save the resolve on the promise itself so we can later resolve it manually | ||
| (promise as any).resolve = resolve; | ||
| setTransitionPromise(promise); | ||
|
|
||
| startTransition(async () => { | ||
| await promise; | ||
| }); | ||
| } | ||
| }} | ||
| > | ||
| {pending ? 'Finish transition' : 'Start transition'} | ||
| </button> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function TransitionSwitcher() { | ||
| const { isLoaded, userMemberships, setActive } = useOrganizationList({ userMemberships: true }); | ||
|
|
||
| if (!isLoaded || !userMemberships.data) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div style={{ display: 'flex', flexDirection: 'row', gap: '10px' }}> | ||
| {userMemberships.data.map(membership => ( | ||
| <TransitionSwitcherButton | ||
| key={membership.organization.id} | ||
| membership={membership} | ||
| setActive={setActive} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function TransitionSwitcherButton({ | ||
| membership, | ||
| setActive, | ||
| }: { | ||
| membership: OrganizationMembershipResource; | ||
| setActive: SetActive; | ||
| }) { | ||
| const [pending, startTransition] = useTransition(); | ||
| return ( | ||
| <button | ||
| onClick={() => { | ||
| startTransition(async () => { | ||
| // Note that this does not currently work, as setActive does not support transitions, | ||
| // we are using it to verify the existing behavior. | ||
| await setActive({ organization: membership.organization.id }); | ||
| }); | ||
| }} | ||
| > | ||
| {pending ? 'Switching...' : `Switch to ${membership.organization.name} in transition`} | ||
| </button> | ||
| ); | ||
| } | ||
|
|
||
| function AuthStatePresenter() { | ||
| const { orgId, sessionId, userId } = useAuth(); | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1>Auth state</h1> | ||
| <div> | ||
| SessionId: <span data-testid='session-id'>{String(sessionId)}</span> | ||
| </div> | ||
| <div> | ||
| UserId: <span data-testid='user-id'>{String(userId)}</span> | ||
| </div> | ||
| <div> | ||
| OrgId: <span data-testid='org-id'>{String(orgId)}</span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function Fetcher() { | ||
| const { orgId } = useAuth(); | ||
|
|
||
| if (!orgId) { | ||
| return null; | ||
| } | ||
|
|
||
| const promise = getCachedPromise('fetcher', orgId, 1000); | ||
| if (promise && (promise as any).status !== 'fulfilled') { | ||
| throw promise; | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1>Fetcher</h1> | ||
| <div data-testid='fetcher-result'>{(promise as any).value}</div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.