Conversation
Cross-route state bleeding when navigating between routes that both use useQueryState with different defaults and setState during render.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The setState-during-render pattern crashes the React Router v7 production server, taking down 87 other tests. Using useEffect still reproduces the core state bleeding issue.
Wire up the cross-route state bleeding reproduction into Next.js (app + pages), Remix, React Router v6, and TanStack Router.
The point of the reproduction is to crash — the fix needs to be in nuqs core, not in user code.
Analysis of #1358 root causeThe bug flowWhen Route A (default
Root cause:
|
| Issue | Framework | Root cause | Fix |
|---|---|---|---|
| #1156 | Next.js App Router | Queued queries not cleared after navigation | Set autoResetQueueOnUpdate: true (PR #1320) |
| #947 | React Router / Remix | Search params updated before navigation completed | Wrap state updates in startTransition() (PR #1316) |
| #1293 | React Router | Cross-page state bleeding on back/forward | Same startTransition fix |
| #524 | Next.js Pages | Both source and target pages re-rendered during navigation | Pages router keeps both mounted during transition |
| #1245 | React Router v6 | <Navigate /> updates search params during first render, never syncs |
Open — same pattern as #1358 |
| #1099 | Next.js / React Router | State temporarily flashes old value | Queue reset timing fix |
Suggested investigation path
The most direct avenue is whether setting autoResetQueueOnUpdate: true in the React Router adapter (like was done for Next.js App Router in PR #1320) resolves #1358 without breaking the existing flush-after-navigate tests. The flush-after-navigate tests specifically exercise debounce/throttle queue behavior across navigations, so they'd be the canary for regressions.
The cross-hook sync emitter firing synchronously during render (line 306) is the deeper issue causing the React warning, but addressing the queue persistence would at least prevent the state bleeding.
Add typeof window check to prevent the state updater from running on the server where location is not available.
- Also set filter value during render (not just mode), matching the original CodeSandbox reproduction where both setMode and setValue are called in the render body - Use .first() on locators for Next.js cache-components compatibility where Activity keeps both pages in the DOM - Wait for URL stability (mode param in URL) before asserting, since the render-time effect updates the URL after DOM mount - Use expectSearch and toHaveURL predicates to wait for the effect
Same fix as PR #1320 applied for the Next.js app router adapter. Clears the globalThrottleQueue after URL updates so that pending state from the previous route doesn't leak into the next route.
commit: |
Setting it to true broke 8 additional tests (flush-after-navigate and others) while not fixing the back-navigation state leak. The fix needs a different approach.
There was a problem hiding this comment.
Pull request overview
Adds an end-to-end reproduction for nuqs issue #1358 (cross-route query state bleeding when setState occurs during render), and introduces queue/adapter changes intended to prevent stale queued updates from leaking across route transitions (notably on popstate/back navigation).
Changes:
- Add shared Playwright e2e spec + per-framework route wiring for repro #1358 across React Router v6/v7, Remix, TanStack Router, and Next.js.
- Introduce a
frozenmode on the global throttle queue and new “silent” queue reset behavior to avoid render-triggered cross-route sync during popstate. - Update React Router and TanStack Router adapters to freeze/reset queues on route transitions to mitigate state bleeding.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/nuqs/src/lib/queues/throttle.ts | Adds frozen flag to drop queue pushes during cross-route navigation windows. |
| packages/nuqs/src/lib/queues/reset.ts | Adjusts queue reset behavior and adds silentResetQueues() to avoid queuedQuerySync emissions. |
| packages/nuqs/src/lib/queues/debounce.ts | Removes queuedQuerySync emission from abortAll() (now handled by resetQueues()). |
| packages/nuqs/src/adapters/tanstack-router.ts | Adds pathname-change handling that freezes/resets queues and strips carried-over search params. |
| packages/nuqs/src/adapters/lib/react-router.ts | Adds popstate-driven pathname-change handling to freeze/reset queue on back/forward nav. |
| packages/nuqs/src/adapters/lib/patch-history.ts | Tracks popstate via a module flag; switches popstate handler to silentResetQueues(). |
| packages/e2e/tanstack-router/src/routes/repro-1358.a.tsx | Adds TanStack Router route A for repro. |
| packages/e2e/tanstack-router/src/routes/repro-1358.b.tsx | Adds TanStack Router route B for repro. |
| packages/e2e/tanstack-router/specs/shared/repro-1358.spec.ts | Wires shared repro spec into TanStack Router e2e. |
| packages/e2e/shared/specs/repro-1358.tsx | Adds shared repro components for Route A/Route B using useQueryState. |
| packages/e2e/shared/specs/repro-1358.spec.ts | Adds shared Playwright tests for derived values + leak checks (A→B and back). |
| packages/e2e/remix/app/routes/repro-1358.a.tsx | Adds Remix route A for repro. |
| packages/e2e/remix/app/routes/repro-1358.b.tsx | Adds Remix route B for repro. |
| packages/e2e/remix/specs/shared/repro-1358.spec.ts | Wires shared repro spec into Remix e2e. |
| packages/e2e/react-router/v7/app/routes/repro-1358.a.tsx | Adds React Router v7 route A for repro. |
| packages/e2e/react-router/v7/app/routes/repro-1358.b.tsx | Adds React Router v7 route B for repro. |
| packages/e2e/react-router/v7/app/routes.ts | Registers v7 repro routes in route config. |
| packages/e2e/react-router/v7/specs/shared/repro-1358.spec.ts | Wires shared repro spec into React Router v7 e2e. |
| packages/e2e/react-router/v6/src/routes/repro-1358.a.tsx | Adds React Router v6 route A for repro. |
| packages/e2e/react-router/v6/src/routes/repro-1358.b.tsx | Adds React Router v6 route B for repro. |
| packages/e2e/react-router/v6/src/react-router.tsx | Registers v6 repro routes in router config. |
| packages/e2e/react-router/v6/specs/shared/repro-1358.spec.ts | Wires shared repro spec into React Router v6 e2e. |
| packages/e2e/next/src/app/app/repro-1358/a/page.tsx | Adds Next.js app router route A for repro. |
| packages/e2e/next/src/app/app/repro-1358/b/page.tsx | Adds Next.js app router route B for repro. |
| packages/e2e/next/src/pages/pages/repro-1358/a.tsx | Adds Next.js pages router route A for repro. |
| packages/e2e/next/src/pages/pages/repro-1358/b.tsx | Adds Next.js pages router route B for repro. |
| packages/e2e/next/specs/shared/repro-1358.spec.ts | Wires shared repro spec into Next.js (app + pages) e2e. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
React's concurrent rendering can reset transition state in the outgoing route during popstate-driven navigation, causing render-time setState calls to fire with stale values and push them into the shared queue on the wrong pathname. The previous approach used a `frozen` flag on the ThrottledQueue to block these pushes, but that leaked queue internals into the adapter layer. This replaces `frozen` with three scoped guards: the outgoing route can't consume the incoming route's pending popstate data (pathname-scoped lookup), stale queue flushes are dropped (mountedPathname check in updateUrl), and a deferred reset catches any queue repopulation between the synchronous reset and the async flush (popstate-only microtask to avoid breaking v6's synchronous forward navigation).
The TSR adapter needed frozen back on the ThrottleQueue because TanStack Router's <Link> carries search params in the router state (not just the URL), and the isFreshMount → unfreeze → refreeze → setTimeout pattern is the only reliable way to handle the timing mismatch between useRouterState.search and location.search. The react-router adapters don't need frozen since they use patchHistory + mountedPathnameRef + microtask instead.
Summary
Fix cross-route state bleeding with setState during render for React Router & TanStack Router.
Closes #1358.