Skip to content

refactor(router-core): Verify changes are necessary before updating store #4964

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

Conversation

Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Aug 16, 2025

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

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.

Copy link

coderabbitai bot commented Aug 16, 2025

Walkthrough

Refactors router load lifecycle to avoid writing undefined loaderData, compute and conditionally set preload, initialize per-match loaderPromise, and limit cleanup updates. Tests add a resolveAfter helper and lower expected store-update counts to match the new async behavior.

Changes

Cohort / File(s) Summary
Router load lifecycle refinements
packages/router-core/src/router.ts
Guard loaderData writes against undefined; compute nextPreload and only update match.preload when changed; initialize a fresh per-match loaderPromise for preloads; rework end-of-load cleanup (clear pending timeout, async gating for loaderPromise reset, reset dehydrated flag); minimize isFetching/invalid updates by comparing before writing.
Tests: timing & expectations
packages/react-router/tests/store-updates-during-navigation.test.tsx
Add resolveAfter(ms, value) helper and replace inline Promises in beforeLoad/loader with it; adjust multiple test expectations to lower update counts reflecting returned values and reduced store updates.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Router
  participant Match

  App->>Router: navigate()
  Router->>Match: set fresh loaderPromise (for preload/load)
  Router->>Match: run beforeLoad / loader (async or sync)
  alt loaderData !== undefined
    Router->>Match: write loaderData
  end
  Router->>Router: compute nextPreload
  alt nextPreload != match.preload
    Router->>Match: update preload
  end
  Router->>Router: clear pending timeout
  alt load was async
    Router->>Match: keep loaderPromise
  else
    Router->>Match: reset loaderPromise
  end
  Router->>Match: reset dehydrated
  Router->>Match: update isFetching/invalid only if changed
  Router-->>App: emit minimal state updates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • schiller-manuel

Poem

I hop through routes with whiskered care,
I stash no void where data should be there.
I tidy preloads, quiet down the chase,
Fewer ripples now across the place.
A nibble, a tweak — the state breathes free. 🥕


📜 Recent 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 db78d61 and 4a45c06.

📒 Files selected for processing (1)
  • packages/router-core/src/router.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router-core/src/router.ts
⏰ 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
✨ 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-check-store-changed-before-updating

🪧 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

nx-cloud bot commented Aug 16, 2025

View your CI Pipeline Execution ↗ for commit 4a45c06

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

☁️ Nx Cloud last updated this comment at 2025-08-16 11:39:51 UTC

Copy link

pkg-pr-new bot commented Aug 16, 2025

More templates

@tanstack/arktype-adapter

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

@tanstack/directive-functions-plugin

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-plugin

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-plugin

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-server-functions-client

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

@tanstack/start-server-functions-fetcher

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

@tanstack/start-server-functions-server

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/zod-adapter

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

commit: 4a45c06

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 (3)
packages/router-core/src/router.ts (1)

2842-2854: Preload computation + conditional update looks good; minor nit on early loaderPromise creation

Computing nextPreload and only updating when it differs reduces store churn. One nit: match._nonReactive.loaderPromise is created unconditionally even when no loader will run and we’ll resolve it immediately later. Consider lazily creating it only in the branches that actually run a loader to trim unnecessary promise churn. Not a blocker.

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

107-110: Type the helper to return the awaited value

resolveAfter currently returns Promise<void> but resolves with value. This compiles due to loose typing but is misleading and weakens type-safety.

Apply this diff:

-function resolveAfter(ms: number, value: any) {
-  return new Promise<void>((resolve) => setTimeout(() => resolve(value), ms))
-}
+function resolveAfter<T>(ms: number, value: T): Promise<T> {
+  return new Promise<T>((resolve) => setTimeout(() => resolve(value), ms))
+}

170-172: Deflake the “nothing” test to avoid timing variance

The >=>= range and “flaky” note suggest timer/tick race. Consider using fake timers to control pendingMs/pendingMinMs timing and eliminate nondeterminism:

  • vi.useFakeTimers() before the test
  • Advance deterministically: vi.advanceTimersByTime(...)
  • await Promise.resolve() between advances if needed to flush microtasks

Not required for this PR, but it will make this assertion stable.

📜 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 1ccf7c8 and 05b5122.

📒 Files selected for processing (2)
  • packages/react-router/tests/store-updates-during-navigation.test.tsx (6 hunks)
  • packages/router-core/src/router.ts (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/router-core/src/router.ts (4)
packages/router-core/src/utils.ts (1)
  • createControlledPromise (398-422)
packages/router-core/src/index.ts (1)
  • createControlledPromise (278-278)
packages/react-router/src/index.tsx (1)
  • createControlledPromise (35-35)
packages/solid-router/src/index.tsx (1)
  • createControlledPromise (35-35)
⏰ 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 (2)

2901-2914: LGTM: precise end-of-load cleanup reduces unnecessary store writes

  • Clearing pendingTimeout and resetting its field unconditionally
  • Only resetting loaderPromise when not running async
  • Resetting dehydrated
  • Change-aware isFetching/invalid update

All of this helps minimize redundant updates and avoids flicker.


2690-2706: Preserve the existing undefined‐guard to avoid unintended data clears

The current if (loaderData !== undefined) check is intentional: many loaders (especially those with a curly‐block body and no explicit return) implicitly return undefined, and we rely on that to prevent child or auxiliary loaders from wiping out previously fetched data. Applying the suggested change would clear valid loaderData whenever a loader doesn’t explicitly return a value—breaking existing tests and expected behavior.

→ Retain the loaderData !== undefined guard as is.

Likely an incorrect or invalid review comment.

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

125-125: Updated expectations align with fewer store updates

The reduced update counts match the router-core’s conditional store writes and async handling tweaks. Looks good.

Also applies to: 143-144, 159-159, 170-172, 211-211

@Sheraff Sheraff merged commit f70688f into main Aug 16, 2025
6 checks passed
@Sheraff Sheraff deleted the refactor-router-core-check-store-changed-before-updating branch August 16, 2025 12:03
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.

2 participants