Skip to content

feat(react-router): history-aware links via preferBack prop on Link#7700

Open
KurtGokhan wants to merge 4 commits into
TanStack:mainfrom
KurtGokhan:feat/prefer-back-links
Open

feat(react-router): history-aware links via preferBack prop on Link#7700
KurtGokhan wants to merge 4 commits into
TanStack:mainfrom
KurtGokhan:feat/prefer-back-links

Conversation

@KurtGokhan

@KurtGokhan KurtGokhan commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

What

Adds a preferBack prop to Link that 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.

<Link to="/issues" preferBack>Back to issues</Link>

Discussion: #7699

Why

A normal <Link to="/issues">Back to issues</Link> always pushes, even when /issues is 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 call history.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

  • Target is the previous entry → history.back().
  • Otherwise → behaves like a normal link (push, or replace if set).
  • Previous entry unknown (fresh load / deep link) → falls back to a normal push. Degrades gracefully; never wrong.
  • Always renders a real <a href>; only a primary, unmodified click is intercepted, so keyboard nav, "copy link", and middle/modifier-click keep working.
  • Match mode: true / 'pathname' (default) matches by pathname only (restores the previous entry's exact search + scroll); 'exact' also requires search to match.

Public API

  • router-core: in-memory per-index history-entry tracking, fed from the existing updateLatestLocation chokepoint (SSR-safe, no window access), exposed via router.getHistoryEntry(index).
  • react-router: useIsBackNavigation(options, match?) hook — the primitive behind preferBack, usable for custom links/buttons.
  • preferBack?: boolean | 'pathname' | 'exact' on Link / LinkOptions.

Layering: per-index tracking (router-core) → useIsBackNavigation (react-router) → preferBack on Link.

Tests

  • packages/router-core/tests/historyEntries.test.ts — tracking by index, replace reusing an index, start-of-history, unknown index, searchStr capture.
  • packages/react-router/tests/useIsBackNavigation.test.tsx — hook decision (back/push/unknown), pathname vs 'exact' modes, preferBack back-vs-push, and click guards (middle-click, modifier-click, user preventDefault), plus <a href> preservation.

Local verification: router-core and react-router unit suites + type tests (tsc) pass; eslint clean on changed files.

Notes

  • Scoped to React first. The core tracking is framework-agnostic, so Solid/Vue parity is a natural follow-up.
  • The per-index tracking is intentionally reusable — a sibling effort ("replay view transitions on browser back/forward") can build on the same map, while this feature stays independently shippable.
  • Marked draft pending direction confirmation from the discussion (API surface / naming / match-mode shape).

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)

  1. preferBack on Link as a prop vs hook-only / helper?
  2. Naming — preferBack?
  3. Match surface — boolean | 'pathname' | 'exact' vs boolean-only (with 'exact' on the hook)?
  4. Any concerns exposing 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

  • New Features
    • Added history-aware link navigation via preferBack, matching by pathname or (optionally) exact pathname + search.
    • Introduced useIsBackNavigation hook to detect whether a navigation target maps to the previous history entry.
    • Added getHistoryEntry to inspect previously recorded history entries.
  • Documentation
    • Updated navigation guide and router API docs for preferBack and useIsBackNavigation.
  • Bug Fixes
    • Preserved native anchor behavior while enabling back navigation when eligible, with safe fallbacks.
  • Tests / Examples
    • Added coverage for back-navigation behavior and updated the basic example to use preferBack.

`<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
@coderabbitai

coderabbitai Bot commented Jun 26, 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51231cf5-34b4-4fd5-9352-1d9a09081ea5

📥 Commits

Reviewing files that changed from the base of the PR and between e9fb62d and be42371.

📒 Files selected for processing (5)
  • docs/router/guide/navigation.md
  • packages/react-router/src/link.tsx
  • packages/react-router/src/useIsBackNavigation.ts
  • packages/react-router/tests/useIsBackNavigation.test.tsx
  • packages/router-core/src/link.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/router-core/src/link.ts
  • packages/react-router/src/useIsBackNavigation.ts
  • packages/react-router/src/link.tsx
  • packages/react-router/tests/useIsBackNavigation.test.tsx
  • docs/router/guide/navigation.md

📝 Walkthrough

Walkthrough

This 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 preferBack and its match modes.

Changes

History-aware back navigation

Layer / File(s) Summary
Router-core history tracking
packages/router-core/src/router.ts, packages/router-core/src/link.ts, packages/router-core/src/index.ts, packages/router-core/tests/historyEntries.test.ts, .changeset/direction-aware-links-prefer-back.md
RouterHistoryEntry and historyEntries are added in router-core, getHistoryEntry reads recorded entries by index, LinkOptionsProps gains preferBack, and tests cover recording, replacement, and lookup behavior.
React-router back navigation
packages/react-router/src/useIsBackNavigation.ts, packages/react-router/src/link.tsx, packages/react-router/src/index.tsx, packages/react-router/tests/useIsBackNavigation.test.tsx
useIsBackNavigation compares the current location with the previous entry, useLinkProps uses preferBack to choose between history.back() and normal navigation, and tests cover match modes and click guards.
Docs and example usage
docs/router/api/router.md, docs/router/api/router/useIsBackNavigation.md, docs/router/guide/navigation.md, examples/react/basic/src/main.tsx, examples/react/basic/src/posts.ts, .changeset/direction-aware-links-prefer-back.md
The API index, hook page, and navigation guide document useIsBackNavigation and preferBack, and the basic example adds a back link while fetching more posts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

package: react-router, package: router-core

Suggested reviewers

  • Sheraff

Poem

🐇 I hopped the trail to yesterday's gate,
and found the back path before too late.
One little click, then backwards I flew,
with anchors and carrots and history too.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: history-aware Link navigation via the new preferBack prop.
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.
✨ 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.

@KurtGokhan KurtGokhan force-pushed the feat/prefer-back-links branch from 890c4b3 to be7f0c7 Compare June 26, 2026 00:29
@KurtGokhan KurtGokhan marked this pull request as ready for review June 26, 2026 09:50

@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: 4

🧹 Nitpick comments (1)
packages/router-core/src/link.ts (1)

694-700: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mention replace in the fallback docs.

This public JSDoc says the fallback is a normal push, but preferBack falls back to normal navigation, including replace when 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

📥 Commits

Reviewing files that changed from the base of the PR and between bb2daa6 and be7f0c7.

📒 Files selected for processing (14)
  • .changeset/direction-aware-links-prefer-back.md
  • docs/router/api/router.md
  • docs/router/api/router/useIsBackNavigation.md
  • docs/router/guide/navigation.md
  • examples/react/basic/src/main.tsx
  • examples/react/basic/src/posts.ts
  • packages/react-router/src/index.tsx
  • packages/react-router/src/link.tsx
  • packages/react-router/src/useIsBackNavigation.ts
  • packages/react-router/tests/useIsBackNavigation.test.tsx
  • packages/router-core/src/index.ts
  • packages/router-core/src/link.ts
  • packages/router-core/src/router.ts
  • packages/router-core/tests/historyEntries.test.ts

Comment thread docs/router/guide/navigation.md Outdated
Comment thread packages/react-router/src/link.tsx
Comment thread packages/react-router/src/link.tsx
Comment thread packages/react-router/src/useIsBackNavigation.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.
@KurtGokhan KurtGokhan force-pushed the feat/prefer-back-links branch from 0662562 to e9fb62d Compare June 26, 2026 10:06
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.

1 participant