feat(react-router): history-aware links via preferBack prop on Link#7700
feat(react-router): history-aware links via preferBack prop on Link#7700KurtGokhan wants to merge 4 commits into
preferBack prop on Link#7700Conversation
`<Link to="/x" preferBack>` navigates via `history.back()` when `/x` resolves to the previous history entry, instead of always pushing a new one. This preserves forward history and the browser's native per-entry scroll restoration for "Back to X" links. Best-effort: falls back to a normal push (or replace) when the target isn't the previous entry or the previous entry is unknown (fresh load / deep link). Always renders a real `<a href>`, so keyboard nav, "copy link", and middle/modifier-click keep working. `preferBack` accepts a match mode: `true`/`'pathname'` (default) matches by pathname only (restoring the previous entry's exact search + scroll), `'exact'` also requires search to match. New public APIs: - router-core: in-memory per-index history tracking + `router.getHistoryEntry(index)` - react-router: `useIsBackNavigation(options, match?)` hook (the primitive behind `preferBack`) Includes tests (router-core tracking; react-router decision + click guards), docs, and a changeset. Discussion: TanStack#7699
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughWalkthroughThis PR adds history-aware back navigation for links and a hook that detects whether a target matches the previous history entry. Router-core records visited history entries, and docs, tests, and an example were updated for ChangesHistory-aware back navigation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 |
890c4b3 to
be7f0c7
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/router-core/src/link.ts (1)
694-700: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMention
replacein the fallback docs.This public JSDoc says the fallback is a normal push, but
preferBackfalls back to normal navigation, includingreplacewhen configured. Tightening the wording here will keep IntelliSense aligned with the actual contract.Suggested wording
/** * Makes the link history-aware: when its target is the previous history entry, * clicking goes back instead of pushing (preserving forward history + scroll). - * `'exact'` also requires search to match; falls back to a normal push otherwise. + * `'exact'` also requires search to match; otherwise it falls back to normal + * navigation (`push`, or `replace` when configured). * `@default` false */🤖 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/router-core/src/link.ts` around lines 694 - 700, The JSDoc for preferBack in link.ts says the fallback is a normal push, but it should also mention that fallback follows the configured replace behavior. Update the comment on preferBack so IntelliSense reflects that when the history-aware back condition does not match, navigation falls back to normal navigation including replace when enabled, rather than implying push-only behavior.
🤖 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 `@docs/router/guide/navigation.md`:
- Around line 136-137: The LinkOptions API docs are inconsistent because
preferBack is shown as only boolean even though the public contract also accepts
'pathname' and 'exact'. Update the LinkOptions snippet in navigation.md to
document all supported match modes, and make sure the nearby history-aware links
explanation and the preferBack symbol description stay aligned with the actual
LinkOptions behavior.
In `@packages/react-router/src/link.tsx`:
- Around line 417-427: The back-navigation detection in link.tsx can become
stale because the location subscription only tracks href while isBackNavigation
also depends on currentLocation.state.__TSR_index. Update the subscription used
by the Link logic so it also re-runs when the history index changes on same-URL
entries, ensuring resolveIsBackNavigation and the preferBack handling recompute
correctly and the link chooses the right navigation action.
- Around line 642-648: The `preferBack` early return in `Link` is bypassing
`reloadDocument`, so links with both flags never reach `router.navigate(...)`
and incorrectly use `router.history.back()` instead. Update the `Link`
navigation logic to check `reloadDocument` before the `isBackNavigation`
shortcut, or explicitly reject the `preferBack` + `reloadDocument` combination
so the behavior is consistent. Use the `Link` component’s navigation branch and
the `isBackNavigation`, `reloadDocument`, and `router.navigate` symbols to keep
the fix localized.
In `@packages/react-router/src/useIsBackNavigation.ts`:
- Around line 71-75: The current location subscription in useIsBackNavigation
only compares href, so it can miss updates when the URL stays the same but
currentLocation.state.__TSR_index changes. Update the equality logic used by
useStore for currentLocation so it also tracks __TSR_index (alongside href) and
re-renders when either changes, ensuring resolveIsBackNavigation always sees the
latest history state.
---
Nitpick comments:
In `@packages/router-core/src/link.ts`:
- Around line 694-700: The JSDoc for preferBack in link.ts says the fallback is
a normal push, but it should also mention that fallback follows the configured
replace behavior. Update the comment on preferBack so IntelliSense reflects that
when the history-aware back condition does not match, navigation falls back to
normal navigation including replace when enabled, rather than implying push-only
behavior.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro
Run ID: d41f40b3-353e-4ab0-b730-9e2a77dc0d6d
📒 Files selected for processing (14)
.changeset/direction-aware-links-prefer-back.mddocs/router/api/router.mddocs/router/api/router/useIsBackNavigation.mddocs/router/guide/navigation.mdexamples/react/basic/src/main.tsxexamples/react/basic/src/posts.tspackages/react-router/src/index.tsxpackages/react-router/src/link.tsxpackages/react-router/src/useIsBackNavigation.tspackages/react-router/tests/useIsBackNavigation.test.tsxpackages/router-core/src/index.tspackages/router-core/src/link.tspackages/router-core/src/router.tspackages/router-core/tests/historyEntries.test.ts
…ze hook options - preferBack: when the back path is taken, forward the link's `viewTransition` intent to the popstate-driven navigation (set `router.shouldViewTransition` before `history.back()`), so a `viewTransition` link behaves consistently on both its push and pop paths instead of only honoring `defaultViewTransition`. - useIsBackNavigation: memoize the navigation options on their primitive fields (mirroring `useLinkProps`) so `buildLocation` no longer re-runs on every render when callers pass a fresh inline options object. - Add a test asserting the link's `viewTransition` is set when going back.
0662562 to
e9fb62d
Compare
What
Adds a
preferBackprop toLinkthat makes "Back to X" links history-aware: when the link's resolved target is the previous history entry, clicking it goes back (history.back()) instead of pushing a new entry.Discussion: #7699
Why
A normal
<Link to="/issues">Back to issues</Link>always pushes, even when/issuesis the entry the user just came from. That throws away forward history and the browser's native per-entry scroll restoration. You can't just callhistory.back()unconditionally (deep links / refresh / arrived-from-elsewhere would break), and the browser doesn't expose the previous entry's location — so the router is the right place to make this decision.Behavior
history.back().replaceif set).<a href>; only a primary, unmodified click is intercepted, so keyboard nav, "copy link", and middle/modifier-click keep working.true/'pathname'(default) matches by pathname only (restores the previous entry's exact search + scroll);'exact'also requires search to match.Public API
updateLatestLocationchokepoint (SSR-safe, nowindowaccess), exposed viarouter.getHistoryEntry(index).useIsBackNavigation(options, match?)hook — the primitive behindpreferBack, usable for custom links/buttons.preferBack?: boolean | 'pathname' | 'exact'onLink/LinkOptions.Layering: per-index tracking (router-core) →
useIsBackNavigation(react-router) →preferBackonLink.Tests
packages/router-core/tests/historyEntries.test.ts— tracking by index, replace reusing an index, start-of-history, unknown index,searchStrcapture.packages/react-router/tests/useIsBackNavigation.test.tsx— hook decision (back/push/unknown), pathname vs'exact'modes,preferBackback-vs-push, and click guards (middle-click, modifier-click, userpreventDefault), plus<a href>preservation.Local verification:
router-coreandreact-routerunit suites + type tests (tsc) pass; eslint clean on changed files.Notes
Demo
Will be testable at Stackblitz after pr.new deploys.
Here is a basic video demo:
Google.Chrome-Vite.App-000783.mp4
Open questions (from the discussion)
preferBackonLinkas a prop vs hook-only / helper?preferBack?boolean | 'pathname' | 'exact'vs boolean-only (with'exact'on the hook)?router.getHistoryEntry()publicly?AI Usage Disclosure
I used AI for generating the changes in this PR, and most of the PR description. But the initial plan was mine and I reviewed the plan and every single line of code committed, and tried to steer AI away from making bad decisions.
Summary by CodeRabbit
preferBack, matching by pathname or (optionally) exact pathname + search.useIsBackNavigationhook to detect whether a navigation target maps to the previous history entry.getHistoryEntryto inspect previously recorded history entries.preferBackanduseIsBackNavigation.preferBack.