Skip to content

fix(nextjs): defer the post-setActive refresh until router transitions settle - #9406

Open
manovotny wants to merge 2 commits into
mainfrom
manovotny/github-issue-9405-2acfc2
Open

fix(nextjs): defer the post-setActive refresh until router transitions settle#9406
manovotny wants to merge 2 commits into
mainfrom
manovotny/github-issue-9405-2acfc2

Conversation

@manovotny

@manovotny manovotny commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Clerk's post-auth navigation into a route whose Server Component calls redirect() hung the App Router on the intermediate route: Rendering… forever, URL parked, signed-out UI. The router.refresh() we dispatch from __internal_onAfterSetActive lands while Next is still following the server redirect. In dev, Next's redirect boundary dispatches the follow-up navigation twice (Strict Mode double effects), the duplicate discards the first queue entry, and the action queue's last pointer is left on the discarded node. Our refresh gets appended behind that dead node, never runs, and the unresolved state promise it handed to React suspends the router forever. Everything on Clerk's side completes; the wedge is the refresh timing.

ClerkProvider now routes the post-setActive refresh through a new useDeferredRefresh hook. A request sets a pending flag on window and starts an empty transition; the refresh dispatches once isPending settles. An empty transition can't settle while the redirect follow-up is still rendering, so the refresh always reaches an idle action queue. The hook is fire-and-forget — setActive must not block on unrelated app-held transitions (transitions.test.ts pins that auth state changes apply immediately mid-transition), so onAfterSetActive keeps its original void contract. Concurrent requests coalesce into one refresh, and the flag survives ClerkProvider remounts, so a request made mid-remount is dispatched by the successor instance.

To test: run https://github.com/manovotny/clerk-nextjs-post-auth-redirect-hang against this build and click "1. Sign in → /setup". Before, it hung on /setup; now it lands on /landed with the signed-in header. setActive during a held startTransition resolves in ~57ms with the refresh parked until the transition finishes.

The queue bookkeeping is a known Next.js bug, fixed upstream in next@16.3.0 via vercel/next.js#95391 (verified: the repro on stock 16.3.0 lands even with unfixed @clerk/nextjs). It remains broken on every 15.5.1–15.5.23 and 16.0–16.2.12 release, and 16.2.x/15.5.x patch releases cut after the fix did not backport it — so this change is what protects the Next versions most apps are actually on.

Fixes #9405

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

…s settle

Clerk's post-auth navigation into a route whose Server Component calls
redirect() wedged the App Router on the intermediate route. The refresh
dispatched from __internal_onAfterSetActive landed while Next was still
following the redirect, got appended behind a discarded entry in the
router action queue, and never ran — leaving React suspended on its
unresolved state promise.

ClerkProvider now routes that refresh through useAwaitableRefresh, which
starts an empty transition and dispatches router.refresh() only once
isPending settles, so the refresh always reaches an idle action queue.

Fixes #9405

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
swingset Ready Ready Preview Aug 12, 2026 2:30am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
clerk-js-sandbox Skipped Skipped Aug 12, 2026 2:30am

Request Review

@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 33d1ff5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/nextjs Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9406

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9406

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9406

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9406

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9406

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9406

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9406

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9406

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9406

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9406

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9406

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9406

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9406

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9406

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9406

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9406

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9406

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9406

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9406

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9406

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9406

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9406

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9406

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9406

commit: 33d1ff5

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T02:42:37.472Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on 33d1ff5.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds useDeferredRefresh, which stores refresh requests on window, waits for active React transitions to settle, and then calls router.refresh(). NextClientClerkProvider uses this callback after authentication state changes. The window type declaration defines the shared refresh state. Tests cover coalescing, remounts, pending requests, and later refreshes. A patch changeset documents the fix.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • clerk/javascript/9405 — Directly addresses the post-authentication App Router hang by deferring router.refresh() until route transitions settle.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main fix: deferring the post-setActive refresh until router transitions settle.
Description check ✅ Passed The description directly explains the App Router hang, the deferred refresh implementation, affected behavior, testing, and issue addressed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nextjs/src/app-router/client/useAwaitableRefresh.ts`:
- Around line 51-54: Update useAwaitableRefresh cleanup so unmounting a
ClerkProvider instance preserves queued refresh callbacks for a successor hook
and does not flush the window buffer prematurely; only resolve callbacks without
dispatching router.refresh() after permanent teardown is established. In
packages/nextjs/src/app-router/client/__tests__/useAwaitableRefresh.test.tsx
lines 97-109, add the remount scenario verifying one refresh dispatch occurs
before promise resolution and update the immediate-unmount assertion for the
final teardown behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: a562e132-30c0-47d1-96f0-0959a9bfd118

📥 Commits

Reviewing files that changed from the base of the PR and between 5e5fe0d and 3611e9c.

📒 Files selected for processing (5)
  • .changeset/defer-refresh-after-set-active.md
  • packages/nextjs/src/app-router/client/ClerkProvider.tsx
  • packages/nextjs/src/app-router/client/__tests__/useAwaitableRefresh.test.tsx
  • packages/nextjs/src/app-router/client/useAwaitableRefresh.ts
  • packages/nextjs/src/global.d.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
  • clerk/clerk-ios (auto-detected)
  • clerk/cli (auto-detected)
  • clerk/clerk-android (auto-detected)

Comment thread packages/nextjs/src/app-router/client/useAwaitableRefresh.ts Outdated
@manovotny
manovotny marked this pull request as draft August 12, 2026 01:27
Awaiting the deferred refresh made setActive block on unrelated
long-running transitions: the empty transition used to detect settling
cannot finish while an app-held transition is pending, which broke the
pinned behavior that auth state changes apply immediately mid-transition
(transitions.test.ts).

The refresh is now requested via a window-stored pending flag and
dispatched by whichever hook instance observes transitions settling, so
nothing awaits it (restoring onAfterSetActive's original void contract)
and a request survives ClerkProvider remounts instead of resolving
without ever running.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel
vercel Bot temporarily deployed to Preview – clerk-js-sandbox August 12, 2026 02:27 Inactive
@manovotny
manovotny marked this pull request as ready for review August 12, 2026 02:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nextjs/src/app-router/client/__tests__/useDeferredRefresh.test.tsx`:
- Around line 38-48: Strengthen the test around the deferred refresh behavior in
the existing useDeferredRefresh test by controlling the transition’s pending
state. Keep isPending true after refresh() is requested and assert mockRefresh
has zero calls, then settle the transition and assert it is called exactly once.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 88439700-5b18-4657-b806-ba535f113566

📥 Commits

Reviewing files that changed from the base of the PR and between 3611e9c and 33d1ff5.

📒 Files selected for processing (4)
  • packages/nextjs/src/app-router/client/ClerkProvider.tsx
  • packages/nextjs/src/app-router/client/__tests__/useDeferredRefresh.test.tsx
  • packages/nextjs/src/app-router/client/useDeferredRefresh.ts
  • packages/nextjs/src/global.d.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
  • clerk/clerk-ios (auto-detected)
  • clerk/cli (auto-detected)
  • clerk/clerk-android (auto-detected)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/nextjs/src/app-router/client/ClerkProvider.tsx

Comment on lines +38 to +48
it('dispatches router.refresh once transitions settle', async () => {
render(<Harness />);

act(() => {
refresh();
});

await waitFor(() => {
expect(mockRefresh).toHaveBeenCalledTimes(1);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test the pending-transition gate.

The test at Line 38 only proves that a requested refresh eventually runs. It does not keep isPending true and assert that router.refresh() does not run until it becomes false. A direct router.refresh() implementation would pass this test, so the regression condition described in this PR is not covered.

Add a controlled pending-transition test. Assert zero calls while the transition is pending. Then settle the transition and assert one call.

As per coding guidelines, “Unit tests are required for all new functionality” and “Include tests for all new features.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/app-router/client/__tests__/useDeferredRefresh.test.tsx`
around lines 38 - 48, Strengthen the test around the deferred refresh behavior
in the existing useDeferredRefresh test by controlling the transition’s pending
state. Keep isPending true after refresh() is requested and assert mockRefresh
has zero calls, then settle the transition and assert it is called exactly once.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Post-auth navigation into a server-redirecting route hangs the App Router

1 participant