From 58358ce9b523434bfbcfae3e148507db32bde344 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Oct 2025 11:56:21 +0300 Subject: [PATCH 1/4] spike: batch state updates in setActive --- packages/clerk-js/src/core/clerk.ts | 61 +++++++++++++++++-- .../src/app-router/client/ClerkProvider.tsx | 2 + .../src/contexts/ClerkContextProvider.tsx | 21 +++++-- packages/types/src/clerk.ts | 4 ++ 4 files changed, 79 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d9c3d53aef8..5adbd96ceb8 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -21,7 +21,7 @@ import { TelemetryCollector, } from '@clerk/shared/telemetry'; import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; -import { allSettled, handleValueOrFn, noop } from '@clerk/shared/utils'; +import { allSettled, createDeferredPromise, handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __experimental_CheckoutInstance, __experimental_CheckoutOptions, @@ -261,6 +261,21 @@ export class Clerk implements ClerkInterface { public __internal_isWebAuthnSupported: (() => boolean) | undefined; public __internal_isWebAuthnAutofillSupported: (() => Promise) | undefined; public __internal_isWebAuthnPlatformAuthenticatorSupported: (() => Promise) | undefined; + public __internal_startTransition = (cb: () => Promise | void): void => { + const sT = this.#options.__internal_startTransition; + // console.log('sT', sT); + if (sT) { + sT(cb); + } else { + void cb(); + } + }; + public __internal_setResources: ListenerCallback = (resources: Resources) => { + const sR = this.#options.__internal_setResources; + if (sR) { + sR(resources); + } + }; public __internal_setActiveInProgress = false; @@ -1400,6 +1415,17 @@ export class Clerk implements ClerkInterface { newSession?.currentTask && this.#options.taskUrls?.[newSession?.currentTask.key]; + const navigatePromise = createDeferredPromise(); + + const transitionSafe = async (promise: Promise | unknown): Promise => { + if (this.#options.__internal_startTransition) { + void Promise.resolve(promise).then(navigatePromise.resolve); + } else { + await promise; + } + }; + + // this.__internal_startTransition(async () => { if (!beforeEmit && (redirectUrl || taskUrl || setActiveNavigate)) { await tracker.track(async () => { if (!this.client) { @@ -1408,29 +1434,43 @@ export class Clerk implements ClerkInterface { } if (newSession?.status !== 'pending') { + // this.__internal_startTransition(() => { this.#setTransitiveState(); + // }); } if (taskUrl) { const taskUrlWithRedirect = redirectUrl ? buildURL({ base: taskUrl, hashSearchParams: { redirectUrl } }, { stringify: true }) : taskUrl; + await this.navigate(taskUrlWithRedirect); } else if (setActiveNavigate && newSession) { - await setActiveNavigate({ session: newSession }); + // this.__internal_startTransition(() => { + await transitionSafe(setActiveNavigate({ session: newSession })); + // void Promise.resolve(setActiveNavigate({ session: newSession })).then(navigatePromise.resolve); + // }); } else if (redirectUrl) { + // if (!this.client) { + // return; + // } if (this.client.isEligibleForTouch()) { const absoluteRedirectUrl = new URL(redirectUrl, window.location.href); const redirectUrlWithAuth = this.buildUrlWithAuth( this.client.buildTouchUrl({ redirectUrl: absoluteRedirectUrl }), ); - await this.navigate(redirectUrlWithAuth); + await transitionSafe(this.navigate(redirectUrlWithAuth)); + // void this.navigate(redirectUrlWithAuth).then(navigatePromise.resolve); } - await this.navigate(redirectUrl); + await transitionSafe(this.navigate(redirectUrl)); + // void this.navigate(redirectUrl).then(navigatePromise.resolve); } }); + } else { + navigatePromise.resolve(); } + // ATTENTION: This breaks for transitions but should be fine. //3. Check if hard reloading (onbeforeunload). If not, set the user/session and emit if (tracker.isUnloading()) { return; @@ -1439,6 +1479,8 @@ export class Clerk implements ClerkInterface { this.#setAccessors(newSession); this.#emit(); + // Await the navigation and the state update + await navigatePromise.promise; // Do not revalidate server cache for pending sessions to avoid unmount of `SignIn/SignUp` AIOs when navigating to task // newSession can be mutated by the time we get here (org change session touch) if (newSession?.status !== 'pending') { @@ -1447,6 +1489,7 @@ export class Clerk implements ClerkInterface { } finally { this.__internal_setActiveInProgress = false; } + console.log('setActive done'); }; public addListener = (listener: ListenerCallback): UnsubscribeCallback => { @@ -1507,6 +1550,8 @@ export class Clerk implements ClerkInterface { const customNavigate = options?.replace && this.#options.routerReplace ? this.#options.routerReplace : this.#options.routerPush; + // console.log('customNavigate', customNavigate); + debugLogger.info(`Clerk is navigating to: ${toURL}`); if (this.#options.routerDebug) { console.log(`Clerk is navigating to: ${toURL}`); @@ -2810,6 +2855,13 @@ export class Clerk implements ClerkInterface { organization: this.organization, }); } + + // this.__internal_setResources?.({ + // client: this.client, + // session: this.session, + // user: this.user, + // organization: this.organization, + // }); } }; @@ -2828,6 +2880,7 @@ export class Clerk implements ClerkInterface { this.session = undefined; this.organization = undefined; this.user = undefined; + console.log('setTransitiveState'); this.#emit(); }; diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index cb3f398fe0a..ee962b41892 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -124,6 +124,8 @@ const NextClientClerkProvider = (props: NextClerkProviderProps) => { routerPush: push, // @ts-expect-error Error because of the stricter types of internal `replace` routerReplace: replace, + // @ts-expect-error Error because of the stricter types of internal `startTransition` + __internal_startTransition: startTransition, }); return ( diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index f38f9f785f5..341c58ede86 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -7,7 +7,7 @@ import { UserContext, } from '@clerk/shared/react'; import type { ClientResource, InitialState, Resources } from '@clerk/types'; -import React from 'react'; +import React, { useDeferredValue } from 'react'; import { IsomorphicClerk } from '../isomorphicClerk'; import type { IsomorphicClerkOptions } from '../types'; @@ -24,17 +24,28 @@ export type ClerkContextProviderState = Resources; export function ClerkContextProvider(props: ClerkContextProvider) { const { isomorphicClerkOptions, initialState, children } = props; - const { isomorphicClerk: clerk, clerkStatus } = useLoadedIsomorphicClerk(isomorphicClerkOptions); + const [updatedState, setState] = React.useState(null); + const { isomorphicClerk: clerk, clerkStatus } = useLoadedIsomorphicClerk({ + ...isomorphicClerkOptions, + // __internal_setResources: (resources: Resources) => setState(resources), + }); - const [state, setState] = React.useState({ + const defaultState = { client: clerk.client as ClientResource, session: clerk.session, user: clerk.user, organization: clerk.organization, - }); + }; + + const state = useDeferredValue(updatedState || defaultState); + + // const state = updatedState || defaultState; React.useEffect(() => { - return clerk.addListener(e => setState({ ...e })); + return clerk.addListener(e => { + // console.log('[Listener]', e.organization?.id); + setState(e); + }); }, []); const derivedState = deriveState(clerk.loaded, state, initialState); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index fcf84df542f..65a73529ade 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -1103,6 +1103,10 @@ export type ClerkOptions = ClerkOptionsNavigation & */ __internal_keyless_dismissPrompt?: (() => Promise) | null; + __internal_startTransition?: (cb: () => Promise | void) => void; + + __internal_setResources?: (resources: Resources) => void; + /** * Customize the URL paths users are redirected to after sign-in or sign-up when specific * session tasks need to be completed. From 5b1e0c6830d33073ad5b43865139440a6eaa6c96 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Oct 2025 11:56:56 +0300 Subject: [PATCH 2/4] wip changeset --- .changeset/spotty-shrimps-rule.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/spotty-shrimps-rule.md diff --git a/.changeset/spotty-shrimps-rule.md b/.changeset/spotty-shrimps-rule.md new file mode 100644 index 00000000000..8e7e5a65441 --- /dev/null +++ b/.changeset/spotty-shrimps-rule.md @@ -0,0 +1,8 @@ +--- +'@clerk/clerk-js': minor +'@clerk/nextjs': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +wip From 706a38761cbc532d8742fae52e2e39bb6f8ebc5e Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Oct 2025 14:31:51 +0300 Subject: [PATCH 3/4] useDeferredValue for status and derived state --- packages/react/src/contexts/ClerkContextProvider.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 341c58ede86..3ef572d3508 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -38,17 +38,17 @@ export function ClerkContextProvider(props: ClerkContextProvider) { }; const state = useDeferredValue(updatedState || defaultState); - // const state = updatedState || defaultState; React.useEffect(() => { return clerk.addListener(e => { - // console.log('[Listener]', e.organization?.id); + console.log('[Listener]', e.organization?.id); setState(e); }); }, []); - const derivedState = deriveState(clerk.loaded, state, initialState); + const _derivedState = deriveState(clerk.loaded, state, initialState); + const derivedState = useDeferredValue(_derivedState); const clerkCtx = React.useMemo( () => ({ value: clerk }), [ @@ -124,7 +124,8 @@ export function ClerkContextProvider(props: ClerkContextProvider) { const useLoadedIsomorphicClerk = (options: IsomorphicClerkOptions) => { const isomorphicClerkRef = React.useRef(IsomorphicClerk.getOrCreateInstance(options)); - const [clerkStatus, setClerkStatus] = React.useState(isomorphicClerkRef.current.status); + const [_clerkStatus, setClerkStatus] = React.useState(isomorphicClerkRef.current.status); + const clerkStatus = useDeferredValue(_clerkStatus); React.useEffect(() => { void isomorphicClerkRef.current.__unstable__updateProps({ appearance: options.appearance }); From bfbb8b05aa38e8ba8eb36d1d1d108903eee829fb Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Oct 2025 14:33:09 +0300 Subject: [PATCH 4/4] cleanup --- packages/react/src/contexts/ClerkContextProvider.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 3ef572d3508..e59deb343fc 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -24,20 +24,19 @@ export type ClerkContextProviderState = Resources; export function ClerkContextProvider(props: ClerkContextProvider) { const { isomorphicClerkOptions, initialState, children } = props; - const [updatedState, setState] = React.useState(null); const { isomorphicClerk: clerk, clerkStatus } = useLoadedIsomorphicClerk({ ...isomorphicClerkOptions, // __internal_setResources: (resources: Resources) => setState(resources), }); - const defaultState = { + const [updatedState, setState] = React.useState({ client: clerk.client as ClientResource, session: clerk.session, user: clerk.user, organization: clerk.organization, - }; + }); - const state = useDeferredValue(updatedState || defaultState); + const state = useDeferredValue(updatedState); // const state = updatedState || defaultState; React.useEffect(() => {