fix(react-router): defer useCanGoBack to the server value while hydrating - #8218
fix(react-router): defer useCanGoBack to the server value while hydrating#8218theRizwan wants to merge 1 commit into
Conversation
…ting The server builds a fresh single entry memory history for each request, so `useCanGoBack` renders `false` in the SSR markup. The browser preserves `history.state` across a reload, so after navigating and refreshing the client router starts on an entry whose `__TSR_index` is already non-zero. The client branch read that index during the hydration render, so the hydration output disagreed with the server markup and React reported a hydration mismatch. Gate the client value on hydration, the same way `resolveIsActive` already gates hash matching in `link.tsx`. While hydrating the hook returns `false`, which matches the server, and React then re-renders with the browser's real history index. Client-only renders are unaffected because `useHydrated` reads its client snapshot on the first render when there is no hydration pass.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthrough
ChangesuseCanGoBack hydration
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This change prevents a hydration mismatch by using the server-compatible navigation value until hydration completes, then restoring the browser-derived value without changing client-only rendering. No actionable merge-blocking risk remains after normal checks and review. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🎯 Changes
Fixes #8211.
useCanGoBackproduced a hydration mismatch whenever a server rendered page was refreshed after a client navigation.The server builds a fresh single entry memory history per request (
createMemoryHistory({ initialEntries: [href] })increateStartHandler), so__TSR_indexis always0and the SSR markup says the router cannot go back. The browser preserveshistory.stateacross a reload, so on a refresh of Page 2 the client router starts on an entry whose__TSR_indexis already1. The client branch of the hook read that index during the hydration render, so the hydration output disagreed with the server markup and React reported a recoverable hydration error, exactly as in the reporter's reproducer.The fix gates the client value on hydration, which is the same approach
resolveIsActivealready uses inlink.tsxfor hash matching:While hydrating,
useHydratedreads its server snapshot and the hook returnsfalse, which matches the server markup. React then re-renders with the browser's real history index, so the value is accurate once hydration settles.Client-only rendering is unaffected.
useHydratedis auseSyncExternalStorewhose server snapshot is only consulted during an actual hydration pass, so a plain client render readstrueon its very first render. I confirmed this empirically before relying on it, and both existinguseCanGoBacktests, which assert the value on the first render of a client-only tree, still pass unchanged.Behaviour note
A server rendered app now renders one frame of
falsebefore the real value arrives. That frame is unavoidable, because the server has no access to the browser's history depth, so the only alternative to the mismatch is to render the honest "unknown" value first. I documented this in the hook's Limitations section along with the two ways to avoid a visible flash.Tests
packages/react-router/tests/issue-8211-useCanGoBack-hydration.test.tsxrenders/aboutwith a server router, hydrates it against a client router whose history is two entries deep, and collects recoverable hydration errors. It fails onmainwith one hydration error and passes with this change, then asserts the value flips tocan go backafter hydration.Scope
Only the React adapter is changed, since this is a React and Start report.
packages/solid-router/src/useCanGoBack.tsandpackages/vue-router/src/useCanGoBack.tshave the same shape and are likely affected too, but theiruseHydratedequivalents are alsofalseon a first client-only render, so a naive gate there would regress SPA behaviour and needs therouter.options.ssrguard thatsolid-router'slink.tsxuses. Happy to follow up on those in a separate PR if you would like the parity.✅ Checklist
Local verification:
pnpm nx run @tanstack/react-router:test:unitpasses, 78 files and 1038 testspnpm nx run @tanstack/react-router:test:typespassespnpm nx run @tanstack/react-router:test:eslintpasses with 0 errorsnode scripts/verify-links.tspasses for the docs change🚀 Release Impact
Summary by CodeRabbit
Bug Fixes
useCanGoBackduring hydration so it consistently reportsfalseinitially, preventing hydration mismatches after page refreshes.Documentation
useCanGoBack.