Skip to content

Commit 736314f

Browse files
authored
feat(nextjs,react,shared): Support suspenseful initialState (#7759)
1 parent 0aff70e commit 736314f

File tree

5 files changed

+28
-10
lines changed

5 files changed

+28
-10
lines changed

.changeset/silly-peaches-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/nextjs': patch
3+
'@clerk/shared': patch
4+
'@clerk/react': patch
5+
---

packages/nextjs/src/app-router/server/ClerkProvider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export async function ClerkProvider<TUi extends Ui = Ui>(
3636

3737
const propsWithEnvs = mergeNextClerkPropsWithEnv({
3838
...rest,
39-
initialState: (await statePromiseOrValue) as InitialState | undefined,
39+
// Even though we always cast to InitialState here, this might still be a promise.
40+
// While not reflected in the public types, we do support this for React >= 19 for internal use.
41+
initialState: statePromiseOrValue as InitialState | undefined,
4042
nonce: await noncePromiseOrValue,
4143
});
4244

packages/react/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export type ClerkProviderProps<TUi extends Ui = Ui> = Omit<IsomorphicClerkOption
4141
/**
4242
* Provide an initial state of the Clerk client during server-side rendering. You don't need to set this value yourself unless you're [developing an SDK](https://clerk.com/docs/guides/development/sdk-development/overview).
4343
*/
44-
initialState?: InitialState;
44+
initialState?: InitialState; // For React >= 19, Promise<InitialState> is also supported for internal use, but not reflected in the types
4545
/**
4646
* Indicates to silently fail the initialization process when the publishable keys is not provided, instead of throwing an error.
4747
* @default false

packages/shared/src/react/ClerkContextProvider.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@ type ClerkContextProps = {
1212
clerk: Clerk;
1313
clerkStatus?: ClerkStatus;
1414
children: React.ReactNode;
15-
initialState?: InitialState;
15+
initialState?: InitialState | Promise<InitialState>;
1616
};
1717

1818
export function ClerkContextProvider(props: ClerkContextProps): JSX.Element | null {
1919
const clerk = props.clerk as LoadedClerk;
2020

2121
assertClerkSingletonExists(clerk);
2222

23+
// The initialState hook has the same check, but it's better to fail early
24+
if (props.initialState instanceof Promise && !('use' in React && typeof React.use === 'function')) {
25+
throw new Error('initialState cannot be a promise if React version is less than 19');
26+
}
27+
2328
const clerkCtx = React.useMemo(
2429
() => ({ value: clerk }),
2530
// clerkStatus is a way to control the referential integrity of the clerk object from the outside,

packages/shared/src/react/contexts.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ export { useUserBase as useUserContext } from './hooks/base/useUserBase';
1919
export { useClientBase as useClientContext } from './hooks/base/useClientBase';
2020
export { useSessionBase as useSessionContext } from './hooks/base/useSessionBase';
2121

22-
const [InitialStateContext, _useInitialStateContext] = createContextAndHook<InitialState | undefined>(
23-
'InitialStateContext',
24-
);
22+
const [InitialStateContext, _useInitialStateContext] = createContextAndHook<
23+
InitialState | Promise<InitialState> | undefined
24+
>('InitialStateContext');
2525

2626
/**
2727
* Provides initial Clerk state (session, user, organization data) from server-side rendering
2828
* to child components via React context.
2929
*
30+
* Passing in a promise is only supported for React >= 19.
31+
*
3032
* The initialState is snapshotted on mount and cannot change during the component lifecycle.
3133
*
3234
* Note that different parts of the React tree can use separate InitialStateProvider instances
@@ -37,7 +39,7 @@ export function InitialStateProvider({
3739
initialState,
3840
}: {
3941
children: React.ReactNode;
40-
initialState: InitialState | undefined;
42+
initialState: InitialState | Promise<InitialState> | undefined;
4143
}) {
4244
// The initialState is not allowed to change, we snapshot it to turn that expectation into a guarantee.
4345
// Note that despite this, it could still be different for different parts of the React tree which is fine,
@@ -51,9 +53,13 @@ export function InitialStateProvider({
5153
export function useInitialStateContext(): InitialState | undefined {
5254
const initialState = _useInitialStateContext();
5355

54-
// If we want to support passing initialState as a promise, this is where we would handle that.
55-
// Note that we can not use(promise) as long as we support React 18, so we'll need to have some extra handling
56-
// and throw the promise instead.
56+
if (initialState instanceof Promise) {
57+
if ('use' in React && typeof React.use === 'function') {
58+
return React.use(initialState);
59+
} else {
60+
throw new Error('initialState cannot be a promise if React version is less than 19');
61+
}
62+
}
5763

5864
return initialState;
5965
}

0 commit comments

Comments
 (0)