@@ -5,6 +5,7 @@ import type {
55 GetToken ,
66 JwtPayload ,
77 PendingSessionOptions ,
8+ Resources ,
89 SignOut ,
910 UseAuthReturn ,
1011} from '@clerk/shared/types' ;
@@ -14,9 +15,81 @@ import { useAuthContext } from '../contexts/AuthContext';
1415import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext' ;
1516import { errorThrower } from '../errors/errorThrower' ;
1617import { invalidStateError } from '../errors/messages' ;
18+ import type { IsomorphicClerk } from '../isomorphicClerk' ;
1719import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider' ;
1820import { createGetToken , createSignOut } from './utils' ;
1921
22+ const clerkLoadedSuspenseCache = new WeakMap < IsomorphicClerk , Promise < void > > ( ) ;
23+ const transitiveStateSuspenseCache = new WeakMap < IsomorphicClerk , Promise < void > > ( ) ;
24+
25+ function createClerkLoadedSuspensePromise ( clerk : IsomorphicClerk ) : Promise < void > {
26+ if ( clerk . loaded ) {
27+ return Promise . resolve ( ) ;
28+ }
29+
30+ const existingPromise = clerkLoadedSuspenseCache . get ( clerk ) ;
31+ if ( existingPromise ) {
32+ return existingPromise ;
33+ }
34+
35+ const promise = new Promise < void > ( resolve => {
36+ if ( clerk . loaded ) {
37+ resolve ( ) ;
38+ return ;
39+ }
40+
41+ const unsubscribe = clerk . addListener ( ( payload : Resources ) => {
42+ if (
43+ payload . client ||
44+ payload . session !== undefined ||
45+ payload . user !== undefined ||
46+ payload . organization !== undefined
47+ ) {
48+ if ( clerk . loaded ) {
49+ clerkLoadedSuspenseCache . delete ( clerk ) ;
50+ unsubscribe ( ) ;
51+ resolve ( ) ;
52+ }
53+ }
54+ } ) ;
55+ } ) ;
56+
57+ clerkLoadedSuspenseCache . set ( clerk , promise ) ;
58+ return promise ;
59+ }
60+
61+ function createTransitiveStateSuspensePromise (
62+ clerk : IsomorphicClerk ,
63+ authContext : { sessionId ?: string | null ; userId ?: string | null } ,
64+ ) : Promise < void > {
65+ if ( authContext . sessionId !== undefined || authContext . userId !== undefined ) {
66+ return Promise . resolve ( ) ;
67+ }
68+
69+ const existingPromise = transitiveStateSuspenseCache . get ( clerk ) ;
70+ if ( existingPromise ) {
71+ return existingPromise ;
72+ }
73+
74+ const promise = new Promise < void > ( resolve => {
75+ if ( authContext . sessionId !== undefined || authContext . userId !== undefined ) {
76+ resolve ( ) ;
77+ return ;
78+ }
79+
80+ const unsubscribe = clerk . addListener ( ( payload : Resources ) => {
81+ if ( payload . session !== undefined || payload . user !== undefined ) {
82+ transitiveStateSuspenseCache . delete ( clerk ) ;
83+ unsubscribe ( ) ;
84+ resolve ( ) ;
85+ }
86+ } ) ;
87+ } ) ;
88+
89+ transitiveStateSuspenseCache . set ( clerk , promise ) ;
90+ return promise ;
91+ }
92+
2093/**
2194 * @inline
2295 */
@@ -35,7 +108,7 @@ type UseAuthOptions = Record<string, any> | PendingSessionOptions | undefined |
35108 * @unionReturnHeadings
36109 * ["Initialization", "Signed out", "Signed in (no active organization)", "Signed in (with active organization)"]
37110 *
38- * @param [initialAuthStateOrOptions] - An object containing the initial authentication state or options for the `useAuth()` hook. If not provided, the hook will attempt to derive the state from the context. `treatPendingAsSignedOut` is a boolean that indicates whether pending sessions are considered as signed out or not. Defaults to `true`.
111+ * @param [initialAuthStateOrOptions] - An object containing the initial authentication state or options for the `useAuth()` hook. If not provided, the hook will attempt to derive the state from the context. `treatPendingAsSignedOut` is a boolean that indicates whether pending sessions are considered as signed out or not. Defaults to `true`. `suspense` is a boolean that enables React Suspense behavior - when `true`, the hook will suspend instead of returning `isLoaded: false`. Requires a Suspense boundary. Defaults to `false`.
39112 *
40113 * @function
41114 *
@@ -95,21 +168,38 @@ type UseAuthOptions = Record<string, any> | PendingSessionOptions | undefined |
95168export const useAuth = ( initialAuthStateOrOptions : UseAuthOptions = { } ) : UseAuthReturn => {
96169 useAssertWrappedByClerkProvider ( 'useAuth' ) ;
97170
98- const { treatPendingAsSignedOut, ...rest } = initialAuthStateOrOptions ?? { } ;
171+ const options = initialAuthStateOrOptions ?? { } ;
172+ const suspense = Boolean ( ( options as any ) . suspense ) ;
173+ const treatPendingAsSignedOut =
174+ 'treatPendingAsSignedOut' in options ? ( options . treatPendingAsSignedOut as boolean | undefined ) : undefined ;
175+
176+ const { suspense : _s , treatPendingAsSignedOut : _t , ...rest } = options as Record < string , any > ;
99177 const initialAuthState = rest as any ;
100178
101179 const authContextFromHook = useAuthContext ( ) ;
102180 const isomorphicClerk = useIsomorphicClerkContext ( ) ;
103181 let authContext = authContextFromHook ;
104182
183+ if ( suspense ) {
184+ if ( ! isomorphicClerk . loaded ) {
185+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- React Suspense requires throwing a promise
186+ throw createClerkLoadedSuspensePromise ( isomorphicClerk ) ;
187+ }
188+
189+ if ( authContext . sessionId === undefined && authContext . userId === undefined ) {
190+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- React Suspense requires throwing a promise
191+ throw createTransitiveStateSuspensePromise ( isomorphicClerk , authContext ) ;
192+ }
193+ }
194+
105195 if ( ! isomorphicClerk . loaded && authContext . sessionId === undefined && authContext . userId === undefined ) {
106196 authContext = initialAuthState != null ? initialAuthState : { } ;
107197 }
108198
109- const getToken : GetToken = useCallback ( createGetToken ( isomorphicClerk ) , [ isomorphicClerk ] ) ;
110- const signOut : SignOut = useCallback ( createSignOut ( isomorphicClerk ) , [ isomorphicClerk ] ) ;
199+ const getToken : GetToken = useCallback ( opts => createGetToken ( isomorphicClerk ) ( opts ) , [ isomorphicClerk ] ) ;
200+ const signOut : SignOut = useCallback ( opts => createSignOut ( isomorphicClerk ) ( opts ) , [ isomorphicClerk ] ) ;
111201
112- isomorphicClerk . telemetry ?. record ( eventMethodCalled ( 'useAuth' , { treatPendingAsSignedOut } ) ) ;
202+ isomorphicClerk . telemetry ?. record ( eventMethodCalled ( 'useAuth' , { suspense , treatPendingAsSignedOut } ) ) ;
113203
114204 return useDerivedAuth (
115205 {
0 commit comments