Repository navigation
@clerk/nextjs with cacheComponents: after a root param change (/en → /sk), Clerk.navigate() within /sk never resolves #10539
Description
Preliminary Checks
-
I have reviewed the documentation: https://clerk.com/docs
-
I have searched for existing issues: https://github.com/clerk/javascript/issues
-
I have not already reached out to Clerk support via email or Discord (if you have, no need to open an issue here)
-
This issue is not a question, general help request, or anything other than a bug report directly related to Clerk. Please ask questions in our Discord community: https://clerk.com/discord.
Reproduction
https://github.com/Joozty/clerk-nextjs-activity-repro
Publishable key
Not shared. The reproduction works with any development instance: put your own key in .env.local (see .env.example).
Description
With cacheComponents: true, a client-side navigation that changes a root param keeps the old root layout tree mounted, but hidden in <Activity>. For example, this happens when you click a link from /en to /sk with app/[lang]/layout.jsx. After that, a Clerk.navigate() call that stays in /sk never resolves. setActive() and signOut() wait for navigate(), so sign-in and sign-out can get stuck.
Steps to reproduce:
- Clone the reproduction and run
pnpm install. - Copy
.env.exampleto.env.local. Set the publishable key of your own development instance there. - Run
pnpm devand open http://localhost:3000/en. - Click Test Clerk.navigate(). It shows
resolved. - Click the
/sklink. Then click Test Clerk.navigate() again.
Expected behavior:
resolved, like in step 4.
Actual behavior:
still pending after 3 seconds. The promise never resolves. The button navigates to the current page, so the URL doesn't change.
pnpm buildandpnpm startgive the same result.partialPrefetching: falsegives the same result.- Without
cacheComponents, the promise resolves. (Remove bothcacheComponentsandpartialPrefetchingfromnext.config.mjs.) - On
/sk,await Clerk.navigate('/en')resolves, because it goes to the other language. It also resolves the promise that was stuck.
Why it happens:
- In the browser, Next.js puts the value of
[lang]in the Reactkeyof the segment. So/enand/skare two different React trees. WithcacheComponents, Next.js doesn't unmount the/entree. It hides the tree in<Activity>. Now twoClerkProviders are mounted. useInternalNavFun(packages/nextjs/src/app-router/client/useInternalNavFun.ts) saves its navigation function inwindow.__clerk_internal_navigations[name].funduring render.- The hidden tree still renders.
useRouter()fromnext/navigationreadsLayoutRouterContext, and Next.js gives this context a new value on every navigation. So after each navigation, the hiddenClerkProviderrenders after the visible one. Its function replaces the visible tree's function. - The hidden tree's function starts the navigation with the hidden tree's
startTransition. The promise resolves only when an effect runs: theisPendingeffect of the hidden tree, or the effect that runs when aClerkProvideris shown or hidden. React doesn't run effects in a hidden tree. So the promise resolves only when a navigation shows or hides a tree, for example/sk→/en. - Later calls with the same URL don't navigate at all, because
pendingDestinationis still that URL.
What users see:
I tested this with a real account:
- Open
/en, click/sk, and sign in with email in the modal. The sign-in gets stuck. Clerk creates the session, but the page shows no user button and no Sign in button until you reload. The modal goes back to the current page, sosetActive()waits for a navigation in the same language. - Open
/en, click/sk, click/en, and sign out. The sign-out gets stuck in the same way, becauseafterSignOutUrl(/en) is in the current language. - Open
/en, click/sk, and sign out. This sign-out finishes, but only because of a second bug (clerk-js ignores newClerkProvideroptions after the first load, soafterSignOutUrlkeeps its first value #10538): clerk-js keeps the firstafterSignOutUrl, so the sign-out goes to the other language,/en. - Sign-in with Google is not affected, because it comes back with a full page load.
- With the fix below, both stuck cases finish.
Possible fix:
Save fun in a layout effect instead of during render. React doesn't run layout effects in a hidden tree. When the tree becomes visible, React runs its layout effects again. So fun always belongs to the visible tree:
// useInternalNavFun.ts
import { useSafeLayoutEffect } from '../../client-boundary/hooks/useSafeLayoutEffect';
// Use this instead of the `if (windowNav) { … }` block:
useSafeLayoutEffect(() => {
if (windowNav) {
getClerkNavigationObject(name).fun = (to, opts) => {
// unchanged
};
}
});The reproduction has this change as a pnpm patch. With the patch, Clerk.navigate() resolves after every language switch. The README explains how to apply the patch.
Related:
- clerk-js ignores new
ClerkProvideroptions after the first load, soafterSignOutUrlkeeps its first value #10538 is the second bug in the same reproduction:afterSignOutUrlkeeps its first value. - fix(nextjs,clerk-js): Make useAwaitableNavigate handle navigations be… #2899 and fix(nextjs): Use separate buffers for each awaitable navigation type #3480 moved this navigation function to
windowand set it during render. This design expects only one mountedClerkProvider. - fix(nextjs): Remove usePathname from navigation hook for cacheComponents compat #7989 removed
usePathnamefrom this hook forcacheComponents. Since then, onlyisPendingor aClerkProviderthat mounts or unmounts resolves the waiting promises. - fix(react): Fix stale SSR state during cross-tab sign-out #7865 fixed another promise in this hook that never resolved, but the trigger was different: a second push to the same URL.
- test(nextjs): Add tests for useInternalNavFun navigation promise resolution #7996 added
cacheComponentssign-out tests, and fix(e2e): fix cache-components template build failures #8071 skipped them becausetoBeSignedOut()timed out. They are still skipped onmain. That test app has one root layout without params, so the cause may be different. - @clerk/nextjs: onBeforeSetActive never settles when invalidateCacheAction rejects (e.g. after a redeploy), so setActive and signOut hang forever #9987 also makes
setActive()andsignOut()hang, but the cause is different: there,onBeforeSetActivenever finishes. Its fix, fix(nextjs): settle onBeforeSetActive when cache invalidation fails #10088, doesn't changeuseInternalNavFun, so it won't fix this bug. - Post-auth navigation into a server-redirecting route hangs the App Router #9405 had a similar hang after sign-in, but fix(nextjs): defer the post-setActive refresh until router transitions settle #9406 traced it to a Next.js router bug that Next 16.3.0 fixed.
- [cacheComponents] Activity component route preservation causes significant breakage in application logic, UI behavior and E2E tests vercel/next.js#86577 is the Next.js issue about
cacheComponentskeeping earlier routes, even the previous language, mounted in a hidden<Activity>. - [Bug]: next-themes forces stale theme when Next.js cacheComponents/Activity is enabled (React 19, Next 16) pacocoursey/next-themes#375 is the same kind of bug in next-themes: after a language switch, the hidden
ThemeProviderof the old language writes a stale theme.
Environment
System:
OS: macOS 27.2
CPU: (10) arm64 Apple M4
Memory: 73.23 MB / 32.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.15.0 - /Users/jozef/.volta/tools/image/node/24.15.0/bin/node
npm: 11.12.1 - /Users/jozef/.volta/tools/image/node/24.15.0/bin/npm
pnpm: 12.4.1 - /Users/jozef/.volta/bin/pnpm
Browsers:
Chrome: 156.0.8078.48
Firefox: 159.0
Safari: 27.2
npmPackages:
@clerk/nextjs: 7.9.22 => 7.9.22
next: 16.4.3 => 16.4.3
react: 19.3.0 => 19.3.0
react-dom: 19.3.0 => 19.3.0
Loaded from the Frontend API:
@clerk/clerk-js: 6.41.0 (6.42.0 and main have the same code)
@clerk/ui: 1.42.0
Activity