Skip to content

fix: fix hash scroll restoration precedence#7332

Open
schiller-manuel wants to merge 5 commits intomainfrom
fix-hash-scroll
Open

fix: fix hash scroll restoration precedence#7332
schiller-manuel wants to merge 5 commits intomainfrom
fix-hash-scroll

Conversation

@schiller-manuel
Copy link
Copy Markdown
Contributor

@schiller-manuel schiller-manuel commented May 3, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Hash-based navigation is no longer overridden by stale scroll-restoration entries; navigating to hash-anchored sections behaves correctly across navigations and reloads.
    • Nested scrollable areas reliably restore their scroll positions after reloads while still allowing hash navigation.
    • Hash target scrolling and overall page scroll restoration are consistent across push/replace navigations.
  • Tests

    • Added end-to-end tests validating hash navigation and nested scroll restoration across reloads.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

Removes the standalone post-navigation handleHashScroll, stamps history action onto parsed locations during Router.load, makes window scroll restoration conditional (skips when destination hash should use element scrollIntoView for PUSH/REPLACE), simplifies scroll-restoration APIs/exports, and adds e2e fixtures/tests for hash vs stale-scroll behavior.

Changes

Scroll restoration, history action, and hash navigation

Layer / File(s) Summary
Data Shape / Types
packages/router-core/src/router.ts
Adds historyActionKey symbol and ParsedLocationWithHistoryAction type; extends LoadFn to accept opts.action?: { type: HistoryAction }.
Core Runtime: Router stamping & load
packages/router-core/src/router.ts
Router.load reads opts?.action?.type and, when defined, stamps that HistoryAction onto the parsed location via historyActionKey.
Scroll Restoration Core
packages/router-core/src/scroll-restoration.ts
Makes scrollRestorationCache module-private; restoration computes skipWindowRestore from destination hash, __hashScrollIntoViewOptions, and history action (PUSH
Inline restoration behavior
packages/router-core/src/scroll-restoration-inline.ts
Narrows API to { storageKey, key? }; restores when entries exist; when a window entry is restored it sets windowRestored and returns early; removes behavior option from window.scrollTo calls.
Server script / Embedding
packages/router-core/src/scroll-restoration-script/server.ts
Simplifies inline script options to { storageKey, key? }; default inline payload no longer includes shouldScrollRestoration; router integration calls getScrollRestorationScript with { storageKey, key }.
Public exports / barrel
packages/router-core/src/index.ts
Removes exported scrollRestorationCache; adds exports defaultGetScrollRestorationKey, getElementScrollRestorationEntry, and storageKey; keeps setupScrollRestoration.
Remove legacy helper
packages/router-core/src/hash-scroll.ts
Deletes exported handleHashScroll(router) helper that performed post-navigation hash-based scrollIntoView.
Transitioner wiring
packages/react-router/src/Transitioner.tsx, packages/solid-router/src/Transitioner.tsx, packages/vue-router/src/Transitioner.tsx
Removes handleHashScroll import and the if (changeInfo.hrefChanged) { handleHashScroll(router) } call from transition-resolved handlers; transitions no longer invoke the removed helper.
E2E fixtures & tests
e2e/react-start/scroll-restoration/src/router.tsx, .../routes/(tests)/hash-scroll-repro.tsx, e2e/react-start/scroll-restoration/tests/hash-scroll-repro.spec.ts
Adds getScrollRestorationKey for the repro route, a hash link and nested scrollable test container, and two Playwright tests validating that hash navigation is not overridden by stale scroll restoration and that nested scroller state restores correctly across reloads.
Release metadata
.changeset/brave-dingos-hope.md
Adds changeset describing patch releases and the fix for hash navigation being overridden by stale scroll restoration entries.

Sequence Diagram

sequenceDiagram
    actor User
    participant Router as Router.load()
    participant ParsedLoc as ParsedLocation
    participant Transitioner as Transitioner
    participant ScrollRestore as setupScrollRestoration

    User->>Router: navigate (e.g. PUSH) with opts.action
    Router->>ParsedLoc: stamp historyAction via symbol (historyActionKey)
    Router->>Transitioner: emit navigation events (includes location with stamped action)
    Transitioner->>Router: complete transition (no handleHashScroll call)

    Note over ScrollRestore: During restoration
    ScrollRestore->>ScrollRestore: compute skipWindowRestore:<br/>dest.hash && __hashScrollIntoViewOptions && (PUSH|REPLACE)
    alt skipWindowRestore = true
        ScrollRestore->>User: skip window restore so native hash scrollIntoView can run
    else
        ScrollRestore->>User: restore window scroll from sessionStorage
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped across hashes, light on my feet,
Stamped history softly so restores won't cheat.
Nested scrolls remembered, stale jumps set aside,
Anchors now find their place with tidy pride.
Cheers from a rabbit — smooth scrolls worldwide.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 'fix: fix hash scroll restoration precedence' directly addresses the main change: fixing the precedence of hash scroll restoration so it is no longer overridden by stale scroll restoration entries during navigation.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-hash-scroll

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

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

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 3, 2026

View your CI Pipeline Execution ↗ for commit f2c377a

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 4m 27s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-03 15:44:22 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

🚀 Changeset Version Preview

4 package(s) bumped directly, 19 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/react-router 1.169.1 → 1.169.2 Changeset
@tanstack/router-core 1.169.1 → 1.169.2 Changeset
@tanstack/solid-router 1.169.1 → 1.169.2 Changeset
@tanstack/vue-router 1.169.1 → 1.169.2 Changeset
@tanstack/react-start 1.167.61 → 1.167.62 Dependent
@tanstack/react-start-client 1.166.47 → 1.166.48 Dependent
@tanstack/react-start-rsc 0.0.40 → 0.0.41 Dependent
@tanstack/react-start-server 1.166.50 → 1.166.51 Dependent
@tanstack/router-cli 1.166.40 → 1.166.41 Dependent
@tanstack/router-generator 1.166.39 → 1.166.40 Dependent
@tanstack/router-plugin 1.167.32 → 1.167.33 Dependent
@tanstack/router-vite-plugin 1.166.47 → 1.166.48 Dependent
@tanstack/solid-start 1.167.58 → 1.167.59 Dependent
@tanstack/solid-start-client 1.166.46 → 1.166.47 Dependent
@tanstack/solid-start-server 1.166.49 → 1.166.50 Dependent
@tanstack/start-client-core 1.168.1 → 1.168.2 Dependent
@tanstack/start-plugin-core 1.169.16 → 1.169.17 Dependent
@tanstack/start-server-core 1.167.28 → 1.167.29 Dependent
@tanstack/start-static-server-functions 1.166.40 → 1.166.41 Dependent
@tanstack/start-storage-context 1.166.34 → 1.166.35 Dependent
@tanstack/vue-start 1.167.54 → 1.167.55 Dependent
@tanstack/vue-start-client 1.166.42 → 1.166.43 Dependent
@tanstack/vue-start-server 1.166.45 → 1.166.46 Dependent

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

Bundle Size Benchmarks

  • Commit: 35261948496a
  • Measured at: 2026-05-03T15:40:40.008Z
  • Baseline source: history:35261948496a
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 87.10 KiB -57 B (-0.06%) 273.77 KiB 75.75 KiB ███████████▁
react-router.full 90.67 KiB -8 B (-0.01%) 285.28 KiB 78.68 KiB ███████████▁
solid-router.minimal 35.33 KiB -56 B (-0.15%) 106.08 KiB 31.85 KiB ███████████▁
solid-router.full 40.05 KiB -57 B (-0.14%) 120.30 KiB 36.02 KiB ███████████▁
vue-router.minimal 53.13 KiB -26 B (-0.05%) 151.22 KiB 47.70 KiB ███████████▁
vue-router.full 58.26 KiB -15 B (-0.03%) 167.40 KiB 52.23 KiB ███████████▁
react-start.minimal 101.73 KiB -34 B (-0.03%) 321.93 KiB 88.15 KiB ███████████▁
react-start.full 105.16 KiB -25 B (-0.02%) 332.26 KiB 90.87 KiB ███████████▁
react-start.rsbuild.minimal 99.32 KiB -14 B (-0.01%) 316.33 KiB 85.43 KiB ███████████▁
react-start.rsbuild.full 102.62 KiB -11 B (-0.01%) 326.76 KiB 88.23 KiB ███████████▁
solid-start.minimal 49.38 KiB -21 B (-0.04%) 151.91 KiB 43.59 KiB ███████████▁
solid-start.full 55.16 KiB -39 B (-0.07%) 168.82 KiB 48.56 KiB ███████████▁

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 3, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7332

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7332

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7332

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7332

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7332

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7332

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7332

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7332

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7332

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7332

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7332

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7332

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7332

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7332

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7332

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7332

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7332

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7332

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7332

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7332

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7332

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7332

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7332

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7332

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7332

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7332

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7332

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7332

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7332

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7332

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7332

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7332

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7332

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7332

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7332

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7332

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7332

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7332

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7332

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7332

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7332

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7332

commit: de88c86

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router-core/src/router.ts (1)

2432-2446: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate historyAction through the direct commitLocation → load() fallback.

load() now records opts?.action?.type, but commitLocation() still calls bare this.load() when there are no history subscribers (Line 2230). That means framework-agnostic usage still drops PUSH/REPLACE metadata, so the new hash-vs-restoration precedence fix won't apply on that path.

Suggested fix
-    if (!this.history.subscribers.size) {
-      this.load()
-    }
+    if (!this.history.subscribers.size) {
+      this.load({
+        action: { type: next.replace ? 'REPLACE' : 'PUSH' },
+      })
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/router.ts` around lines 2432 - 2446, The
commitLocation → load() fallback drops the new historyAction
(opts?.action?.type) because commitLocation calls this.load() with no opts;
update commitLocation to capture the location's history action (or derive it the
same way load expects) and call this.load({ action: { type: historyAction } })
so load() receives and propagates historyAction; adjust references in
commitLocation and any call sites that call this.load() directly to pass the
action object so the historyAction-based precedence logic in load (and
setLocationHistoryAction) works consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/router-core/src/scroll-restoration.ts`:
- Around line 152-153: The server setup guard uses the nullish coalescing
operator with isServer (isServer ?? router.isServer) which wrongly ignores
router.isServer when isServer is false; update the condition in the scroll
restoration setup to use logical OR so it reads (isServer || router.isServer)
and keep the existing short-circuit check with router.isScrollRestorationSetup
to prevent setup when either side indicates server context or restoration is
already configured.

---

Outside diff comments:
In `@packages/router-core/src/router.ts`:
- Around line 2432-2446: The commitLocation → load() fallback drops the new
historyAction (opts?.action?.type) because commitLocation calls this.load() with
no opts; update commitLocation to capture the location's history action (or
derive it the same way load expects) and call this.load({ action: { type:
historyAction } }) so load() receives and propagates historyAction; adjust
references in commitLocation and any call sites that call this.load() directly
to pass the action object so the historyAction-based precedence logic in load
(and setLocationHistoryAction) works consistently.
🪄 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: c1e8e065-50fa-4e4f-9fa7-106c646f2187

📥 Commits

Reviewing files that changed from the base of the PR and between 3526194 and 4929dcc.

📒 Files selected for processing (13)
  • .changeset/brave-dingos-hope.md
  • e2e/react-start/scroll-restoration/src/router.tsx
  • e2e/react-start/scroll-restoration/src/routes/(tests)/hash-scroll-repro.tsx
  • e2e/react-start/scroll-restoration/tests/hash-scroll-repro.spec.ts
  • packages/react-router/src/Transitioner.tsx
  • packages/router-core/src/hash-scroll.ts
  • packages/router-core/src/index.ts
  • packages/router-core/src/router.ts
  • packages/router-core/src/scroll-restoration-inline.ts
  • packages/router-core/src/scroll-restoration-script/server.ts
  • packages/router-core/src/scroll-restoration.ts
  • packages/solid-router/src/Transitioner.tsx
  • packages/vue-router/src/Transitioner.tsx
💤 Files with no reviewable changes (2)
  • packages/router-core/src/hash-scroll.ts
  • packages/router-core/src/index.ts

Comment on lines +152 to 153
if ((isServer ?? router.isServer) || router.isScrollRestorationSetup) {
return
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Confirm the exact guard currently used
rg -n "isServer \\?\\? router\\.isServer|isServer \\|\\| router\\.isServer" \
  packages/router-core/src/scroll-restoration.ts -C2

# 2) Inspect isServer declaration(s) to verify expected boolean semantics
rg -n "export\\s+const\\s+isServer\\b|declare\\s+const\\s+isServer\\b" \
  packages --iglob '*isServer*.ts' -C2

Repository: TanStack/router

Length of output: 173


🏁 Script executed:

cat -n packages/router-core/src/scroll-restoration.ts | head -180 | tail -50

Repository: TanStack/router

Length of output: 1930


🏁 Script executed:

sed -n '130,160p' packages/router-core/src/scroll-restoration.ts

Repository: TanStack/router

Length of output: 954


🏁 Script executed:

head -100 packages/router-core/src/scroll-restoration.ts

Repository: TanStack/router

Length of output: 2930


🏁 Script executed:

find packages -name "isServer*" -type f

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

rg "export.*isServer" packages --iglob "*.ts" --iglob "*.tsx" -A 3

Repository: TanStack/router

Length of output: 2640


Use || instead of ?? in the server setup guard.

Line 152 currently uses isServer ?? router.isServer, which ignores router.isServer whenever isServer is false (in client builds). Since isServer can be false, true, or undefined depending on the build target, the nullish coalescing operator skips the router-level server check when the module exports false. The logical OR operator correctly handles all cases: when isServer is false or undefined, it falls back to router.isServer, ensuring that router-level server contexts prevent scroll restoration setup.

Suggested fix
-  if ((isServer ?? router.isServer) || router.isScrollRestorationSetup) {
+  if ((isServer || router.isServer) || router.isScrollRestorationSetup) {
     return
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/scroll-restoration.ts` around lines 152 - 153, The
server setup guard uses the nullish coalescing operator with isServer (isServer
?? router.isServer) which wrongly ignores router.isServer when isServer is
false; update the condition in the scroll restoration setup to use logical OR so
it reads (isServer || router.isServer) and keep the existing short-circuit check
with router.isScrollRestorationSetup to prevent setup when either side indicates
server context or restoration is already configured.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isServer is undefined at development time and either constant true or false for treeshaking. check how isServer is defined!

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the isServer module definition
fd "isServer" packages --type f
echo "---"
# Also check node_modules for the package if available
find . -path "*/router-core/isServer*" -not -path "*/node_modules/*" 2>/dev/null | head -20
echo "---"
# Search for isServer declaration in packages
rg -n "isServer" packages --iglob "*.ts" -l | head -20

Length of output: 918


🏁 Script executed:

#!/bin/bash
cat packages/router-core/src/isServer/server.ts
echo "---server---"
cat packages/router-core/src/isServer/client.ts
echo "---client---"
cat packages/router-core/src/isServer/development.ts
echo "---development---"
# Also check the vite config to see how the export condition is set up
cat packages/router-core/vite.config.ts

Length of output: 1684


@schiller-manuel, you are absolutely right — my original suggestion was incorrect. Here's the breakdown:

Build target isServer value isServer ?? router.isServer isServer || router.isServer
Development / test undefined router.isServer router.isServer
Production server true true (short-circuits) ✓ true (short-circuits) ✓
Production client false false (short-circuits) ✓ router.isServer ← ❌ wrong

In a client build, isServer is definitively false, meaning it is not a server environment. The ?? operator correctly short-circuits to false, allowing scroll restoration setup to proceed. Switching to || would incorrectly fall back to router.isServer in that case, which could block setup if a router instance happened to have router.isServer = true inside a client bundle.

The ?? operator is intentional and correct here. I'll dismiss my comment.


✏️ Learnings added
Learnt from: schiller-manuel
Repo: TanStack/router PR: 7332
File: packages/router-core/src/scroll-restoration.ts:152-153
Timestamp: 2026-05-03T11:34:24.919Z
Learning: In `packages/router-core/src/scroll-restoration.ts`, the guard `(isServer ?? router.isServer)` is intentional and correct. `isServer` is imported from `tanstack/router-core/isServer` and has three possible values: `undefined` in development/test (via `development.ts`), `true` in production server builds (via `server.ts`), and `false` in production client builds (via `client.ts`). The `??` (nullish coalescing) operator is used so that `false` (client build) short-circuits to `false` without falling back to `router.isServer`, while `undefined` (dev/test) correctly falls back to `router.isServer`. Using `||` would be wrong because `false || router.isServer` would add an unnecessary and potentially incorrect fallback in client builds.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 3, 2026

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing fix-hash-scroll (de88c86) with main (3526194)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

nx-cloud[bot]

This comment was marked as outdated.

Co-authored-by: schiller-manuel <schiller-manuel@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router-core/src/router.ts (1)

2424-2447: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate the action through the no-subscriber fallback.

load() only stamps latestLocation when opts.action is present, but commitLocation still falls back to a bare this.load() when history.subscribers.size === 0. That leaves the new hash-scroll precedence signal absent in a reachable navigation path.

🔧 Proposed fix
-        this.load()
+        this.load({ action: { type: next.replace ? 'REPLACE' : 'PUSH' } })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/router.ts` around lines 2424 - 2447, The
no-subscriber fallback path in commitLocation calls this.load() without passing
through the history action stamping, so the historyActionKey is never set on
latestLocation when history.subscribers.size === 0; update the fallback in
commitLocation to call this.load with the same opts (or construct an opts
containing the original action) so that load() can apply the historyAction onto
this.latestLocation using historyActionKey (refer to load, commitLocation,
latestLocation, history.subscribers, historyAction, and historyActionKey)
ensuring the new hash-scroll precedence signal is present in all navigation
paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/router-core/src/router.ts`:
- Around line 2424-2447: The no-subscriber fallback path in commitLocation calls
this.load() without passing through the history action stamping, so the
historyActionKey is never set on latestLocation when history.subscribers.size
=== 0; update the fallback in commitLocation to call this.load with the same
opts (or construct an opts containing the original action) so that load() can
apply the historyAction onto this.latestLocation using historyActionKey (refer
to load, commitLocation, latestLocation, history.subscribers, historyAction, and
historyActionKey) ensuring the new hash-scroll precedence signal is present in
all navigation paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5f5f95c-01c5-47c0-9512-508f9e3dd835

📥 Commits

Reviewing files that changed from the base of the PR and between dd562e4 and f2c377a.

📒 Files selected for processing (1)
  • packages/router-core/src/router.ts

nx-cloud[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud has identified a flaky task in your failed CI:

🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.

Nx Cloud View detailed reasoning in Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

@schiller-manuel schiller-manuel requested a review from Sheraff May 3, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant