-
Notifications
You must be signed in to change notification settings - Fork 408
poc: Remove contexts and subscribe to clerk changes directly in hooks #7267
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: vincent-and-the-doctor
Are you sure you want to change the base?
Changes from all commits
bc169ba
3fa0633
e5d4d4b
3b6687e
11a38da
cf02da1
9f9477d
c7ed168
c4eadb4
2f45577
09d8a8a
31d4f2b
7a5381c
a23789b
ba26f9c
d72ec7c
19b996b
a2adbf3
c85a66c
f35bbbb
288cf94
5d87895
a8f9f60
f37d8d1
d3d85d1
5b81912
c3c79f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,7 @@ | ||
| import type { InitialState, Without } from '@clerk/shared/types'; | ||
| import { headers } from 'next/headers'; | ||
| import type { ReactNode } from 'react'; | ||
| import React from 'react'; | ||
|
|
||
| import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthProvider'; | ||
| import { getDynamicAuthData } from '../../server/buildClerkProps'; | ||
| import type { NextClerkProviderProps } from '../../types'; | ||
| import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; | ||
|
|
@@ -32,28 +30,17 @@ export async function ClerkProvider( | |
| ) { | ||
| const { children, dynamic, ...rest } = props; | ||
|
|
||
| async function generateStatePromise() { | ||
| if (!dynamic) { | ||
| return Promise.resolve(null); | ||
| } | ||
| return getDynamicClerkState(); | ||
| } | ||
|
|
||
| async function generateNonce() { | ||
| if (!dynamic) { | ||
| return Promise.resolve(''); | ||
| } | ||
| return getNonceHeaders(); | ||
| } | ||
| const statePromiseOrValue = dynamic ? getDynamicClerkState() : undefined; | ||
| const noncePromiseOrValue = dynamic ? getNonceHeaders() : ''; | ||
|
|
||
| const propsWithEnvs = mergeNextClerkPropsWithEnv({ | ||
| ...rest, | ||
| initialState: statePromiseOrValue as InitialState | Promise<InitialState> | undefined, | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not awaiting this at the top level might be considered a breaking change. I'm not sure it is technically, but it might change the loading experience of apps, so we should be careful. We could also do this incrementally and optionally by introducing something like |
||
| nonce: await noncePromiseOrValue, | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awaiting this is something we might also want to move further down. |
||
| }); | ||
|
|
||
| const { shouldRunAsKeyless, runningWithClaimedKeys } = await getKeylessStatus(propsWithEnvs); | ||
|
|
||
| let output: ReactNode; | ||
|
|
||
| try { | ||
| const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then( | ||
| mod => mod.detectKeylessEnvDrift, | ||
|
|
@@ -64,35 +51,15 @@ export async function ClerkProvider( | |
| } | ||
|
|
||
| if (shouldRunAsKeyless) { | ||
| output = ( | ||
| return ( | ||
| <KeylessProvider | ||
| rest={propsWithEnvs} | ||
| generateNonce={generateNonce} | ||
| generateStatePromise={generateStatePromise} | ||
| runningWithClaimedKeys={runningWithClaimedKeys} | ||
| > | ||
| {children} | ||
| </KeylessProvider> | ||
| ); | ||
| } else { | ||
| output = ( | ||
| <ClientClerkProvider | ||
| {...propsWithEnvs} | ||
| nonce={await generateNonce()} | ||
| initialState={await generateStatePromise()} | ||
| > | ||
| {children} | ||
| </ClientClerkProvider> | ||
| ); | ||
| } | ||
|
|
||
| if (dynamic) { | ||
| return ( | ||
| // TODO: fix types so AuthObject is compatible with InitialState | ||
| <PromisifiedAuthProvider authPromise={generateStatePromise() as unknown as Promise<InitialState>}> | ||
| {output} | ||
| </PromisifiedAuthProvider> | ||
| ); | ||
| } | ||
| return output; | ||
| return <ClientClerkProvider {...propsWithEnvs}>{children}</ClientClerkProvider>; | ||
| } | ||
This file was deleted.
This file was deleted.
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should likely be renamed now that it doesn't actually contain a context. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import type { DeriveStateReturnType } from '@clerk/shared/deriveState'; | ||
| import { deriveFromClientSideState, deriveFromSsrInitialState } from '@clerk/shared/deriveState'; | ||
| import { useClerkInstanceContext, useInitialStateContext } from '@clerk/shared/react'; | ||
| import type { | ||
| ActClaim, | ||
| ClientResource, | ||
| JwtPayload, | ||
| OrganizationCustomPermissionKey, | ||
| OrganizationCustomRoleKey, | ||
| SessionStatusClaim, | ||
| } from '@clerk/shared/types'; | ||
| import { useCallback, useDeferredValue, useMemo, useSyncExternalStore } from 'react'; | ||
|
|
||
| type AuthStateValue = { | ||
| userId: string | null | undefined; | ||
| sessionId: string | null | undefined; | ||
| sessionStatus: SessionStatusClaim | null | undefined; | ||
| sessionClaims: JwtPayload | null | undefined; | ||
| actor: ActClaim | null | undefined; | ||
| orgId: string | null | undefined; | ||
| orgRole: OrganizationCustomRoleKey | null | undefined; | ||
| orgSlug: string | null | undefined; | ||
| orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; | ||
| factorVerificationAge: [number, number] | null; | ||
| }; | ||
|
|
||
| export const defaultDerivedInitialState = { | ||
| actor: undefined, | ||
| factorVerificationAge: null, | ||
| orgId: undefined, | ||
| orgPermissions: undefined, | ||
| orgRole: undefined, | ||
| orgSlug: undefined, | ||
| sessionClaims: undefined, | ||
| sessionId: undefined, | ||
| sessionStatus: undefined, | ||
| userId: undefined, | ||
| }; | ||
|
|
||
| export function useAuthState(): AuthStateValue { | ||
| const clerk = useClerkInstanceContext(); | ||
| const initialStateContext = useInitialStateContext(); | ||
| // If we make initialState support a promise in the future, this is where we would use() that promise | ||
| const initialSnapshot = useMemo(() => { | ||
| if (!initialStateContext) { | ||
| return defaultDerivedInitialState; | ||
| } | ||
| const fullState = deriveFromSsrInitialState(initialStateContext); | ||
| return authStateFromFull(fullState); | ||
| }, [initialStateContext]); | ||
|
|
||
| const snapshot = useMemo(() => { | ||
| if (!clerk.loaded) { | ||
| return initialSnapshot; | ||
| } | ||
| const state = { | ||
| client: clerk.client as ClientResource, | ||
| session: clerk.session, | ||
| user: clerk.user, | ||
| organization: clerk.organization, | ||
| }; | ||
| const fullState = deriveFromClientSideState(state); | ||
| return authStateFromFull(fullState); | ||
| }, [clerk.client, clerk.session, clerk.user, clerk.organization, initialSnapshot, clerk.loaded]); | ||
|
|
||
| const authState = useSyncExternalStore( | ||
| useCallback(callback => clerk.addListener(callback, { skipInitialEmit: true }), [clerk]), | ||
| useCallback(() => snapshot, [snapshot]), | ||
| useCallback(() => initialSnapshot, [initialSnapshot]), | ||
| ); | ||
|
|
||
| // If an updates comes in during a transition, uSES usually deopts that transition to be synchronous, | ||
| // which for example means that already mounted <Suspense> boundaries might suddenly show their fallback. | ||
| // This makes all auth state changes into transitions, but does not deopt to be synchronous. If it's | ||
| // called during a transition, it immediately uses the new value without deferring. | ||
| return useDeferredValue(authState); | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a common pattern for all the hooks and is a breaking change. Doing this by default in the This does not by itself get rid of the transitive state. To do that safely we probably have to start the transition ourselves from inside |
||
| } | ||
|
|
||
| function authStateFromFull(derivedState: DeriveStateReturnType) { | ||
| return { | ||
| sessionId: derivedState.sessionId, | ||
| sessionStatus: derivedState.sessionStatus, | ||
| sessionClaims: derivedState.sessionClaims, | ||
| userId: derivedState.userId, | ||
| actor: derivedState.actor, | ||
| orgId: derivedState.orgId, | ||
| orgRole: derivedState.orgRole, | ||
| orgSlug: derivedState.orgSlug, | ||
| orgPermissions: derivedState.orgPermissions, | ||
| factorVerificationAge: derivedState.factorVerificationAge, | ||
| }; | ||
| } | ||
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.
uSESreads state directly fromclerk, so this was introduced to avoid extra emits every timeaddListenerwas called in a hook.