Skip to content

fix(react-router): defer useCanGoBack to the server value while hydrating - #8218

Open
theRizwan wants to merge 1 commit into
TanStack:mainfrom
theRizwan:fix/use-can-go-back-hydration
Open

fix(react-router): defer useCanGoBack to the server value while hydrating#8218
theRizwan wants to merge 1 commit into
TanStack:mainfrom
theRizwan:fix/use-can-go-back-hydration

Conversation

@theRizwan

@theRizwan theRizwan commented Sep 2, 2026

Copy link
Copy Markdown

🎯 Changes

Fixes #8211.

useCanGoBack produced 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] }) in createStartHandler), so __TSR_index is always 0 and the SSR markup says the router cannot go back. The browser preserves history.state across a reload, so on a refresh of Page 2 the client router starts on an entry whose __TSR_index is already 1. 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 resolveIsActive already uses in link.tsx for hash matching:

const isHydrated = useHydrated()
const canGoBack = useStore(
  router.stores.location,
  (location) => location.state.__TSR_index !== 0,
)
return isHydrated && canGoBack

While hydrating, useHydrated reads its server snapshot and the hook returns false, 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. useHydrated is a useSyncExternalStore whose server snapshot is only consulted during an actual hydration pass, so a plain client render reads true on its very first render. I confirmed this empirically before relying on it, and both existing useCanGoBack tests, 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 false before 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.tsx renders /about with a server router, hydrates it against a client router whose history is two entries deep, and collects recoverable hydration errors. It fails on main with one hydration error and passes with this change, then asserts the value flips to can go back after hydration.

Scope

Only the React adapter is changed, since this is a React and Start report. packages/solid-router/src/useCanGoBack.ts and packages/vue-router/src/useCanGoBack.ts have the same shape and are likely affected too, but their useHydrated equivalents are also false on a first client-only render, so a naive gate there would regress SPA behaviour and needs the router.options.ssr guard that solid-router's link.tsx uses. Happy to follow up on those in a separate PR if you would like the parity.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with the relevant test commands, or tests do not apply to this pull request.
  • I fully understand the code in this pull request, including any code generated with AI assistance.

Local verification:

  • pnpm nx run @tanstack/react-router:test:unit passes, 78 files and 1038 tests
  • pnpm nx run @tanstack/react-router:test:types passes
  • pnpm nx run @tanstack/react-router:test:eslint passes with 0 errors
  • node scripts/verify-links.ts passes for the docs change

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes

    • Fixed useCanGoBack during hydration so it consistently reports false initially, preventing hydration mismatches after page refreshes.
    • The hook now reflects the browser’s actual history once hydration completes, correctly indicating when users can navigate back.
  • Documentation

    • Clarified server-rendering and hydration behavior for useCanGoBack.
    • Added guidance for keeping dependent layouts stable while the accurate history state becomes available.

…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.
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: e50fbba8-9a4e-40a2-9a20-aee95682a709

📥 Commits

Reviewing files that changed from the base of the PR and between 37877da and 26e92eb.

📒 Files selected for processing (4)
  • .changeset/use-can-go-back-hydration.md
  • docs/router/api/router/useCanGoBack.md
  • packages/react-router/src/useCanGoBack.ts
  • packages/react-router/tests/issue-8211-useCanGoBack-hydration.test.tsx

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

useCanGoBack now returns false during server rendering and hydration, then reports browser history after hydration. Documentation describes the behavior, and a regression test verifies hydration without errors.

Changes

useCanGoBack hydration

Layer / File(s) Summary
Hydration-safe hook behavior
packages/react-router/src/useCanGoBack.ts, docs/router/api/router/useCanGoBack.md, .changeset/use-can-go-back-hydration.md
The client hook returns false until useHydrated() settles, then reports the browser history state. Documentation and release metadata describe the behavior.
Hydration regression coverage
packages/react-router/tests/issue-8211-useCanGoBack-hydration.test.tsx
The test hydrates server markup with a deeper client history, verifies that no recoverable hydration mismatch occurs, and confirms that useCanGoBack eventually returns true.

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

Merge Risk: ⚪ Minimal · up to 26e92

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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: deferring useCanGoBack to the server value during hydration.
Description check ✅ Passed The description explains the bug, implementation, behavior impact, tests, scope, checklist completion, and release impact. It follows the required template.
Linked Issues check ✅ Passed The changes satisfy issue #8211 by preventing the hydration mismatch while preserving the browser history value after hydration. The regression test verifies both outcomes.
Out of Scope Changes check ✅ Passed The changes are limited to the React hook fix, its regression test, documentation, and the required changeset. These changes directly support issue #8211.
Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useCanGoBack causes hydration error on page refresh

1 participant