Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
bc169ba
Refactor promises in NextJS ClerkProvider
Ephem Nov 10, 2025
3fa0633
Remove PromisifiedAuth
Ephem Nov 10, 2025
e5d4d4b
Remove initialAuthState from useAuth
Ephem Nov 10, 2025
3b6687e
Update expo useAuth to match new signature
Ephem Nov 10, 2025
11a38da
Add changeset
Ephem Nov 10, 2025
cf02da1
Remove special isNext13 handling from Next ClerkProvider
Ephem Nov 11, 2025
9f9477d
Temporarily remove affected packages from changeset
Ephem Nov 11, 2025
c7ed168
Add back affected packages
Ephem Nov 11, 2025
c4eadb4
Merge branch 'vincent-and-the-doctor' into fredrik/remove-initial-aut…
Ephem Nov 11, 2025
2f45577
Fix changeset react package name
Ephem Nov 11, 2025
09d8a8a
Add InitialAuthStateProvider and refactor to derive authState in useA…
Ephem Nov 12, 2025
31d4f2b
Use InitialAuthStateProvider directly for nested Next ClerkProvider
Ephem Nov 12, 2025
7a5381c
Resolve !dynamic without promises
Ephem Nov 13, 2025
a23789b
Merge branch 'vincent-and-the-doctor' into fredrik/remove-initial-aut…
Ephem Nov 18, 2025
ba26f9c
Clean up AuthContext
Ephem Nov 19, 2025
d72ec7c
Merge branch 'vincent-and-the-doctor' into fredrik/remove-initial-aut…
Ephem Nov 19, 2025
19b996b
Merge branch 'vincent-and-the-doctor' into fredrik/remove-initial-aut…
Ephem Nov 19, 2025
a2adbf3
Remove AuthContext and add uSES for useAuth hook
Ephem Nov 19, 2025
c85a66c
Move ClerkContextProvider from ui to shared/react
Ephem Nov 19, 2025
f35bbbb
Update shared ClerkContextProvider to support initialState and switch…
Ephem Nov 19, 2025
288cf94
Remove SessionContext and refactor to uSES
Ephem Nov 20, 2025
5d87895
Remove UserContext and refactor to uSES
Ephem Nov 20, 2025
a8f9f60
Remove OrganizationProvider and refactor to uSES
Ephem Nov 20, 2025
f37d8d1
Remove ClientContext and refactor to uSES
Ephem Nov 20, 2025
d3d85d1
Support passing in initialState as a promise
Ephem Nov 20, 2025
5b81912
Add skipInitialEmit option to addListener and use in uSES
Ephem Nov 20, 2025
c3c79f9
Remove unrelated changeset
Ephem Nov 20, 2025
7b73924
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem Nov 25, 2025
7581b74
Rename setAccessors -> updateAccessors and make it emit
Ephem Nov 26, 2025
05debd5
Fix getSnapshot stale closure issue
Ephem Nov 27, 2025
37eb323
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem Nov 27, 2025
e66fe43
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem Dec 1, 2025
8f23b8b
Update tests
Ephem Dec 1, 2025
dfc6e64
Remove useDeferredValue
Ephem Dec 1, 2025
ba8373e
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem Dec 2, 2025
185844f
Merge branch 'vincent-and-the-doctor' into fredrik/poc
Ephem Dec 4, 2025
f0b43d3
Introduce __internal_lastEmittedResources in clerk-js to enable prope…
Ephem Dec 4, 2025
84fdb9b
Refactor initialState
Ephem Dec 9, 2025
3244b9a
Merge branch 'main' into fredrik/poc
Ephem Dec 9, 2025
9d6f7f9
Revert supporting initialState as a promise (for now)
Ephem Dec 9, 2025
e3b3d7e
Fix mergeNextClerkPropsWithEnv types
Ephem Dec 9, 2025
1fed366
Trigger
Ephem Dec 9, 2025
c59b0bb
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context…
Ephem Dec 10, 2025
7486150
Add basic transition tests
Ephem Dec 10, 2025
c371efe
Remove failing @ts-expect-error in test
Ephem Dec 10, 2025
b4af784
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context…
Ephem Dec 11, 2025
565238b
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context…
Ephem Dec 11, 2025
162e2ec
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context…
Ephem Dec 12, 2025
4feac2c
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context…
Ephem Dec 15, 2025
65d1344
Re-render Components controller when Clerk emits
Ephem Dec 16, 2025
e8efb64
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context…
Ephem Dec 16, 2025
18db369
Merge branch 'main' into fredrik/user-4043-remove-clerk-state-context…
Ephem Dec 17, 2025
9d30d64
Flush internal routing state synchronously to guarantee rerenders hap…
Ephem Dec 17, 2025
b0716b5
Add comment to flushSync
Ephem Dec 17, 2025
1152cea
Add toBeSignedIn checks to transition tests
Ephem Dec 17, 2025
d12f639
Fix conditional hook in RQ implementation
Ephem Dec 17, 2025
0c4909f
Rename useAuthState to useAuthBase to match other hooks
Ephem Dec 18, 2025
5376088
Add skipInitialEmit to JSDoc
Ephem Dec 18, 2025
598c13a
Add changeset
Ephem Dec 18, 2025
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
32 changes: 32 additions & 0 deletions .changeset/full-parents-crash.md
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
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>
);
}
2 changes: 1 addition & 1 deletion integration/testUtils/usersService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export const createUserService = (clerkClient: ClerkClient) => {
const name = faker.animal.dog();
const organization = await withErrorLogging('createOrganization', () =>
clerkClient.organizations.createOrganization({
name: faker.animal.dog(),
name: name,
createdBy: userId,
}),
);
Expand Down
Loading
Loading