Skip to content

refactor(router-core): skip beforeLoad and related store updates if options is not defined #4928

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 16, 2025

Conversation

Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Aug 11, 2025

Following #4916, #4925, and #4926, this PR keeps reducing the number of store updates (__store.setState through updateMatch).

Early bail out of route before load step if route.options.beforeLoad is not defined


The store-updates-during-navigation test tracking the number of executions of a useRouterState > select method during a navigation goes from 17 calls without this PR, to 14 calls with this PR (or even 13 calls if beforeLoad is synchronous).


Should be a partial improvement of #4359

Summary by CodeRabbit

  • New Features

    • Added a not-found helper and router options to configure a default Not Found component and a default preload strategy.
  • Refactor

    • Streamlined the before-load lifecycle for more deterministic pending-state updates, earlier pending-timeout readiness, clearer parameter/search error surfacing, and adjusted context composition; loader error timing is more conservative.
  • Tests

    • Expanded test coverage with new scenarios (sync before-load, not-found flow, hover-preload/navigation) and updated navigation update expectations.

Copy link

nx-cloud bot commented Aug 11, 2025

View your CI Pipeline Execution ↗ for commit 8a7f954

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

☁️ Nx Cloud last updated this comment at 2025-08-16 08:41:06 UTC

Copy link

pkg-pr-new bot commented Aug 11, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@4928

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@4928

@tanstack/eslint-plugin-router

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

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@4928

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@4928

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@4928

@tanstack/react-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-plugin@4928

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@4928

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@4928

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@4928

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@4928

@tanstack/solid-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-plugin@4928

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@4928

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@4928

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@4928

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@4928

@tanstack/start-server-functions-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-client@4928

@tanstack/start-server-functions-fetcher

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-fetcher@4928

@tanstack/start-server-functions-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-server@4928

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@4928

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@4928

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@4928

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@4928

commit: 8a7f954

Copy link

coderabbitai bot commented Aug 15, 2025

Walkthrough

Reworks router-core beforeLoad and pending timing/flow with new helpers, AbortController-based pending lifecycle, earlier parse/validate error surfacing, batched state updates, and minor timing changes; adds tests and uses the new notFound export and router options. All production API signatures unchanged.

Changes

Cohort / File(s) Summary of Changes
Router core: beforeLoad / pending refactor
packages/router-core/src/router.ts
Reworked beforeLoad orchestration: added helpers (shouldExecuteBeforeLoad, executeBeforeLoad, handleBeforeLoad, isBeforeLoadSsr), setupPendingTimeout earlier, create AbortController and pending()/resolve() flow, always invoke pending to render pending state, surface parse/validate errors earlier (PARSE_PARAMS / VALIDATE_SEARCH), support sync and async beforeLoad results, store __beforeLoadContext on match, batch state updates, remove defensive try/catch around triggerOnReady, and avoid awaiting potentialPendingMinPromise in loader error paths unless present.
React Router tests
packages/react-router/tests/store-updates-during-navigation.test.tsx
Expanded tests and adjusted assertions: added import of fireEvent and notFound, used defaultNotFoundComponent and defaultPreload router options in test setup, switched to fireEvent.click for navigation triggers, updated several expected update counts, and added new cases (sync beforeLoad, hover preload then navigate, not-found in beforeLoad, minimal "nothing" case). All changes confined to tests.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Router
  participant Match
  participant BeforeLoad
  participant Loader
  participant Timer as PendingTimeout

  App->>Router: navigate(location, cause)
  Router->>Match: create AbortController, call pending()
  Router->>Timer: setupPendingTimeout()
  alt no beforeLoad
    Router-->>App: batched pending state rendered
  else beforeLoad present
    Router->>BeforeLoad: invoke (sync or async) with beforeLoadFnContext
    alt sync result
      Router-->>Match: merge context, set __beforeLoadContext (batched)
    else async result
      BeforeLoad-->>Router: resolve or throw
      Router-->>Match: merge context or handle BEFORE_LOAD error
    end
  end
  Timer-->>Router: pendingMs elapsed -> triggerOnReady()
  Router->>Loader: start loader (if applicable)
  alt error/redirect/notFound
    Router-->>App: handle error/redirect/notFound flow
  else success
    Router-->>App: render ready state
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Possibly related PRs

Suggested reviewers

  • schiller-manuel

Poem

Hop, hop — I check each route and trace,
Timers set, beforeLoad finds its place.
Sync results land, async waits take flight,
Batched updates tidy up the night.
Tests snugly pass — a carrot-coded sight 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-router-core-skip-before-load

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

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

🧹 Nitpick comments (5)
packages/router-core/src/router.ts (4)

2321-2324: Make pending timeout resilient to async errors from onReady

triggerOnReady can return a Promise. If onReady ever throws/rejects, this can surface as an unhandled rejection in the timeout callback. Guard it defensively.

Apply this diff:

-        // Update the match and prematurely resolve the loadMatches promise so that
-        // the pending component can start rendering
-        this.triggerOnReady(innerLoadContext)
+        // Update the match and prematurely resolve the loadMatches promise so that
+        // the pending component can start rendering
+        const result = this.triggerOnReady(innerLoadContext)
+        if (isPromise(result)) void result.catch(() => {})

2416-2433: Return early after PARSE_PARAMS/VALIDATE_SEARCH errors to avoid unnecessary state churn

We already resolve promises and mark the match as errored in handleSerialError. Continuing (setting pending timers, etc.) is unnecessary and slightly noisy. Early returns here keep the flow tighter and avoid extra updates.

Apply this diff:

     if (paramsError) {
       this.handleSerialError(
         innerLoadContext,
         index,
         paramsError,
         'PARSE_PARAMS',
       )
+      return
     }

     if (searchError) {
       this.handleSerialError(
         innerLoadContext,
         index,
         searchError,
         'VALIDATE_SEARCH',
       )
+      return
     }

2436-2443: Skip fetchCount bump and redundant state updates when no beforeLoad is defined

The bail-out path still invokes pending() which increments fetchCount and assigns a new abortController, then immediately clears fetching in the same batch. Since there's no beforeLoad work, we can resolve the promise without touching the match state, further reducing updates and avoiding a misleading fetchCount bump.

Apply this diff:

-    // if there is no `beforeLoad` option, skip everything, batch update the store, return early
+    // if there is no `beforeLoad` option, skip everything, resolve, return early
     if (!route.options.beforeLoad) {
-      batch(() => {
-        pending()
-        resolve()
-      })
+      resolve()
       return
     }

2462-2471: Guard against null (or non-object) beforeLoad returns when merging context

If a route's beforeLoad mistakenly returns null, the spread operator will throw. Treat undefined/null as "no additional context".

Apply this diff:

-        innerLoadContext.updateMatch(matchId, (prev) => ({
-          ...prev,
-          __beforeLoadContext: beforeLoadContext,
-          context: {
-            ...prev.context,
-            ...beforeLoadContext,
-          },
-        }))
+        innerLoadContext.updateMatch(matchId, (prev) => {
+          const safeCtx =
+            beforeLoadContext && typeof beforeLoadContext === 'object'
+              ? beforeLoadContext
+              : {}
+          return {
+            ...prev,
+            __beforeLoadContext: safeCtx,
+            context: {
+              ...prev.context,
+              ...safeCtx,
+            },
+          }
+        })
packages/react-router/tests/store-updates-during-navigation.test.tsx (1)

134-148: Great addition: sync beforeLoad scenario covered

Covers a key path; expectation of 13 seems consistent with the new lifecycle.

Consider adding a couple of small follow-ups to lock in the bail-out and edge behaviors that this PR targets:

  • A route with no beforeLoad: assert minimal updates (ensures the bail-out truly produces no extra store updates).
  • A beforeLoad that returns undefined: behaves like “no context change”.
  • A beforeLoad that returns null: if you keep the suggested guard, verify it doesn’t throw and produces no context merge.

I can draft these tests if helpful.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9d4b7c2 and 891b55c.

📒 Files selected for processing (2)
  • packages/react-router/tests/store-updates-during-navigation.test.tsx (2 hunks)
  • packages/router-core/src/router.ts (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/router-core/src/router.ts (3)
packages/router-core/src/utils.ts (2)
  • createControlledPromise (398-422)
  • isPromise (477-485)
packages/router-core/src/redirect.ts (1)
  • isRedirect (95-97)
packages/router-core/src/route.ts (1)
  • BeforeLoadContextOptions (966-977)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (3)
packages/router-core/src/router.ts (1)

2724-2726: Loader error path honors minPending only when present — LGTM

Awaiting potentialPendingMinPromise(matchId) conditionally avoids unnecessary waits and matches the intent. Looks good.

packages/react-router/tests/store-updates-during-navigation.test.tsx (2)

113-114: Updated expectation reflects fewer store updates — LGTM

The drop to 14 aligns with the optimized beforeLoad/pending flow.


131-133: Preload redirection update count decreased — LGTM

The new expectation at 6 matches the refined error/pending timing.

Copy link

@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

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

2321-2324: Guard triggerOnReady to avoid unhandled rejections in setTimeout callback

Removing the try/catch means any thrown error or rejected promise from onReady will become an unhandled error in the timeout callback. Wrap the call and catch async rejections to avoid noisy console errors in production and hard-to-reproduce bugs.

-      const pendingTimeout = setTimeout(() => {
-        // Update the match and prematurely resolve the loadMatches promise so that
-        // the pending component can start rendering
-        this.triggerOnReady(innerLoadContext)
-      }, pendingMs)
+      const pendingTimeout = setTimeout(() => {
+        // Update the match and prematurely resolve the loadMatches promise so that
+        // the pending component can start rendering
+        try {
+          const ready = this.triggerOnReady(innerLoadContext)
+          if (isPromise(ready)) {
+            ready.catch((err) => {
+              if (process.env.NODE_ENV !== 'production') {
+                console.error('[router] onReady failed during pending timeout', err)
+              }
+            })
+          }
+        } catch (err) {
+          if (process.env.NODE_ENV !== 'production') {
+            console.error('[router] onReady threw during pending timeout', err)
+          }
+        }
+      }, pendingMs)

2403-2425: Reuse existing abortController instead of swapping in a new instance

Creating a new AbortController here replaces the one established when the match was created. That swaps cancellation tokens mid-flight and can be surprising for integrations holding a reference to the previous controller. Prefer reusing the existing controller when present.

-    const abortController = new AbortController()
+    const abortController = match.abortController ?? new AbortController()
@@
-      innerLoadContext.updateMatch(matchId, (prev) => ({
+      innerLoadContext.updateMatch(matchId, (prev) => ({
         ...prev,
         isFetching: 'beforeLoad',
         fetchCount: prev.fetchCount + 1,
         abortController,
         context,
       }))

2436-2444: Skip updateMatch entirely when no beforeLoad is defined to align with PR goal (fewer store updates)

This early-bail branch still calls pending() and resolve() which each execute updateMatch (batched, but still a state write and fetchCount bump). The PR’s objective is to “skip beforeLoad and related store updates if options is not defined.” You can resolve the internal promise and return without any store mutations; the loader phase will take over setting isFetching as needed.

-    // if there is no `beforeLoad` option, skip everything, batch update the store, return early
-    if (!route.options.beforeLoad) {
-      batch(() => {
-        pending()
-        resolve()
-      })
-      return
-    }
+    // If there is no `beforeLoad` option, resolve and return early with no store writes
+    if (!route.options.beforeLoad) {
+      match._nonReactive.beforeLoadPromise?.resolve()
+      match._nonReactive.beforeLoadPromise = undefined
+      return
+    }

Follow-up: This also avoids an extra fetchCount increment when nothing actually fetched.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 891b55c and 4fd8dfc.

📒 Files selected for processing (1)
  • packages/router-core/src/router.ts (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/router-core/src/router.ts (3)
packages/router-core/src/utils.ts (2)
  • createControlledPromise (398-422)
  • isPromise (477-485)
packages/router-core/src/route.ts (1)
  • BeforeLoadContextOptions (966-977)
packages/router-core/src/redirect.ts (1)
  • isRedirect (95-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (2)
packages/router-core/src/router.ts (2)

2381-2400: Early surfacing of params/search validation errors is solid

Handling paramsError and searchError up-front with explicit router codes (PARSE_PARAMS, VALIDATE_SEARCH) tightens control flow and simplifies downstream error handling. This looks correct and beneficial.


2729-2731: Good: only await pendingMinPromise when present

This reduces unnecessary delay on the error path and keeps timing more predictable.

Copy link

@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

🧹 Nitpick comments (5)
packages/react-router/tests/store-updates-during-navigation.test.tsx (5)

84-86: Router options are appropriate; consider making defaultPreload configurable per test

Setting defaultPreload: 'intent' globally works and is needed for the hover/focus scenario later. To reduce coupling across tests, consider accepting defaultPreload as a setup option so tests not exercising preloading can explicitly opt out. This can help reduce flakiness risk over time if event semantics evolve.


93-101: Prefer findByRole over wrapping getByRole in waitFor

findBy* queries encapsulate waiting and read cleaner. This is functionally equivalent and a small readability win.

Apply:

-  fireEvent.click(link)
-  const title = await waitFor(
-    () => screen.getByRole('heading', { name: /Title$/ }), // matches /posts and /other and not found
-  )
+  fireEvent.click(link)
+  const title = await screen.findByRole('heading', { name: /Title$/ }) // matches /posts and /other and not found

137-141: Optional: add a correctness assertion alongside the update-count metric

In addition to the update-count assertion, consider asserting the side-effect (redirect) to catch regressions where the redirect is swallowed but updates still occur. Example:

-  expect(updates).toBe(6)
+  expect(updates).toBe(6)
+  // Optional: ensure we actually redirected
+  expect(router.state.location.pathname).toBe('/other')

Note: If preloadRoute is designed to not navigate, skip this assertion.


159-169: Good guardrails for minimal path; consider hardening against flakiness

The range assertion acknowledges timing variability. If this ever becomes noisy in CI, two knobs can help:

  • Disable preloading for this test (see suggestion to make defaultPreload configurable).
  • Use fake timers around the 100–300ms pending thresholds to fully control scheduling.

If you want, I can sketch a minimal fake-timers pattern compatible with testing-library + React 18.


186-209: Preload-on-intent scenario is well captured; minor test ergonomics

The focus+click sequence is appropriate for 'intent' preloading, and the assertion targets the metric you care about. Two tiny nits:

  • Prefer findByRole for readability, as above.
  • If you ever want to exercise hover intent specifically, you can also fireEvent.mouseOver(link) (or userEvent.hover) before click.

Apply:

-    const title = await waitFor(() =>
-      screen.getByRole('heading', { name: /Title$/ }),
-    )
+    const title = await screen.findByRole('heading', { name: /Title$/ })
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1532086 and 8a7f954.

📒 Files selected for processing (1)
  • packages/react-router/tests/store-updates-during-navigation.test.tsx (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (4)
packages/react-router/tests/store-updates-during-navigation.test.tsx (4)

2-8: Imports update looks good

The test now consistently uses testing-library’s render/screen/waitFor and cleanup. No issues.


122-122: Expectation update matches the PR’s objective

The reduced update count (17 → 14) reflects the early bail-out when beforeLoad is undefined. Looks good.


143-157: Solid coverage for synchronous beforeLoad path

This specifically validates the optimization delta when beforeLoad is sync. No issues.


171-184: Covers not-found raised in beforeLoad

This ensures the early not-found surfacing path doesn’t inflate updates. Looks correct.

import {
Link,
Outlet,
RouterProvider,
createRootRoute,
createRoute,
createRouter,
notFound,
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Ensure the notFound export is available from the public entry

If notFound is a new public API, verify it’s exported from the package entry and re-exported where this test imports it.

Run this to confirm the export and its surface:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Looking for notFound exports (direct or re-export):"
rg -nP -C2 '(export\s+function\s+notFound\b)|(export\s+const\s+notFound\b)|(export\s*{\s*notFound\s*})' --type ts --glob '!**/node_modules/**'

echo
echo "Verifying createRouter options include defaultNotFoundComponent & defaultPreload:"
rg -nP -C3 '\b(defaultNotFoundComponent|defaultPreload)\b' --type ts --glob '!**/node_modules/**'

Length of output: 82102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Import section from the test file ==="
sed -n '1,50p' packages/react-router/tests/store-updates-during-navigation.test.tsx

echo
echo "=== Re-exports of notFound in react-router public entry (index.ts) ==="
rg -nP 'export\s+.*notFound' packages/react-router/src/index.ts || echo "No direct re-exports found in index.ts"

Length of output: 1446


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Files in packages/react-router/src ==="
ls -1 packages/react-router/src

echo
echo "=== Looking for index.* files ==="
find packages/react-router/src -maxdepth 1 -type f -name "index.*" || echo "No index.* files found"

echo
echo "=== Showing contents of any index.* file ==="
for f in packages/react-router/src/index.*; do
  echo "---- $f ----"
  sed -n '1,200p' "$f"
done 2>/dev/null || true

Length of output: 5057


Missing public export of notFound

The test in
packages/react-router/tests/store-updates-during-navigation.test.tsx
imports notFound, but it isn’t re-exported in the public entry at
packages/react-router/src/index.tsx. Add it to the export list:

• File: packages/react-router/src/index.tsx

-export {
-  defer,
-  TSR_DEFERRED_PROMISE,
-  isMatch,
-  joinPaths,
-  cleanPath,
-  trimPathLeft,
-  trimPathRight,
-  trimPath,
-  resolvePath,
-  parsePathname,
-  interpolatePath,
-  matchPathname,
-  removeBasepath,
-  matchByPath,
-  encode,
-  decode,
-  rootRouteId,
-  defaultSerializeError,
-  defaultParseSearch,
-  defaultStringifySearch,
-  parseSearchWith,
-  stringifySearchWith,
-  escapeJSON, // SSR
-  pick,
-  functionalUpdate,
-  replaceEqualDeep,
-  isPlainObject,
-  isPlainArray,
-  deepEqual,
-  shallow,
-  createControlledPromise,
-  retainSearchParams,
-  stripSearchParams,
-} from '@tanstack/router-core'
+export {
+  defer,
+  TSR_DEFERRED_PROMISE,
+  isMatch,
+  joinPaths,
+  cleanPath,
+  trimPathLeft,
+  trimPathRight,
+  trimPath,
+  resolvePath,
+  parsePathname,
+  interpolatePath,
+  matchPathname,
+  removeBasepath,
+  matchByPath,
+  encode,
+  decode,
+  rootRouteId,
+  defaultSerializeError,
+  defaultParseSearch,
+  defaultStringifySearch,
+  parseSearchWith,
+  stringifySearchWith,
+  escapeJSON, // SSR
+  pick,
+  functionalUpdate,
+  replaceEqualDeep,
+  isPlainObject,
+  isPlainArray,
+  deepEqual,
+  shallow,
+  createControlledPromise,
+  retainSearchParams,
+  stripSearchParams,
+  notFound,
+} from '@tanstack/router-core'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
notFound,
export {
defer,
TSR_DEFERRED_PROMISE,
isMatch,
joinPaths,
cleanPath,
trimPathLeft,
trimPathRight,
trimPath,
resolvePath,
parsePathname,
interpolatePath,
matchPathname,
removeBasepath,
matchByPath,
encode,
decode,
rootRouteId,
defaultSerializeError,
defaultParseSearch,
defaultStringifySearch,
parseSearchWith,
stringifySearchWith,
escapeJSON, // SSR
pick,
functionalUpdate,
replaceEqualDeep,
isPlainObject,
isPlainArray,
deepEqual,
shallow,
createControlledPromise,
retainSearchParams,
stripSearchParams,
notFound,
} from '@tanstack/router-core'
🤖 Prompt for AI Agents
packages/react-router/src/index.tsx (export list): The test imports `notFound`
but it is not exported from the package entry; add a named export for `notFound`
to the public export list in packages/react-router/src/index.tsx so it can be
imported by tests and consumers. Ensure the exported identifier matches the
internal symbol (or re-export from its source module), update the export list
accordingly, and run the test suite to verify the import succeeds.

@Sheraff Sheraff merged commit 405ef6b into main Aug 16, 2025
6 checks passed
@Sheraff Sheraff deleted the refactor-router-core-skip-before-load branch August 16, 2025 08:48
Sheraff added a commit that referenced this pull request Aug 16, 2025
…tore (#4964)

Following #4916,
#4925,
#4926, and
#4928, this PR keeps reducing the
number of store updates (`__store.setState` through `updateMatch`).

We check the values we want to set in the store are *actually different*
from what is already in the store before calling `updateMatch`

---

In the `store-updates-during-navigation` test tracking the number of
executions of a `useRouterState > select` method during a navigation,
our main test case goes from **14 calls** without this PR, to **10
calls** with this PR. Most test cases show significant improvements as
well.

---

Should be a partial improvement of
#4359

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Prevented loss of previously loaded data during navigation when a
loader yields no value.
* Stabilized preloading to avoid redundant updates and unnecessary state
churn.
* Improved loading-state cleanup after navigation, reducing flicker and
inconsistent “fetching” indicators.

* **Performance**
* Reduced unnecessary state updates and re-renders during and after
preloads/loads for smoother navigation.

* **Tests**
* Updated async navigation tests to reflect refined timing and
data-return behavior and added a helper to simulate delayed async
results.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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