Skip to content

fix(router-core): on navigate (with preload), route component isn't rendered with undefined context if beforeLoad is pending #5002

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

longzheng
Copy link

@longzheng longzheng commented Aug 20, 2025

Fixes #4998

Summary by CodeRabbit

  • Bug Fixes

    • Prevents components from rendering with an empty/undefined route context during preloads.
    • Ensures consistent route context between preload and navigation to avoid visual flicker.
    • Refines context merging so route-level context reliably overrides inherited values for predictable behavior.
  • Tests

    • Adds a reproducer test validating preload behavior and context updates during navigation, ensuring no interim empty renders.

…endered with undefined context if beforeLoad is pending
Copy link

coderabbitai bot commented Aug 20, 2025

Walkthrough

Adds a reproducer test for preload/pending route context behavior and updates router-core to include a route's prior __beforeLoadContext when constructing context passed to beforeLoad.

Changes

Cohort / File(s) Summary
Tests: preload/pending route context
packages/react-router/tests/routeContext.test.tsx
Adds a test reproducer validating preload vs enter beforeLoad calls, selector behavior, pending rendering, and that components do not render with an empty/undefined context during preload.
Router core: beforeLoad context merge
packages/router-core/src/load-matches.ts
Changes executeBeforeLoad to merge match.__beforeLoadContext, parentMatchContext, and match.__routeContext (in that order) when building the context passed to beforeLoad and when storing __beforeLoadContext, altering key precedence.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant Router
  participant Matcher as loadMatches/executeBeforeLoad
  participant Route as Route.beforeLoad
  participant View as Route Component
  participant Sel as useRouteContext(selector)

  App->>Router: intent preload to '/foo'
  Router->>Matcher: preload('/foo')
  Note over Matcher: Build context = __beforeLoadContext + parentMatchContext + routeContext
  Matcher->>Route: beforeLoad(cause: 'preload', preload: true)
  Route-->>Matcher: (delayed) resolves context -> store __beforeLoadContext
  Router->>View: (pending) show pendingComponent — route component not rendered with empty context

  App->>Router: navigate/enter '/foo'
  Router->>Matcher: enter('/foo')
  Matcher->>Route: beforeLoad(cause: 'enter', preload: false)
  Route-->>Matcher: resolves context (updated)
  Matcher-->>Router: finalize match context
  Router->>View: render Route Component
  View->>Sel: useRouteContext(selector)
  Sel-->>View: selector receives final context (e.g., foo: 2)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Prevent route component rendering with undefined context during pending/preload (#4998)
Ensure beforeLoad is awaited/its context applied before component render when pendingComponent is used (#4998)
Correctly differentiate beforeLoad cause values for preload vs enter and propagate context updates to selector (#4998)

Possibly related PRs

Suggested reviewers

  • schiller-manuel

Poem

I hop through routes, nose twitching with delight,
Preload hums softly, pending keeps things light.
BeforeLoad stirs context, patient and true,
Then enter arrives — the component sees new.
A rabbit rejoices: no empty fields tonight! 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 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 (2)
packages/router-core/src/load-matches.ts (1)

372-372: Precedence and safety nits: confirm desired override order and guard against undefined spreads

Two minor points to consider:

  • Precedence: With { ...__beforeLoadContext, ...parent, ...__routeContext }, the route’s static __routeContext overrides any overlapping keys from the previously resolved __beforeLoadContext. During an enter when beforeLoad is pending, this could temporarily “hide” values derived during preload if keys overlap. If the intent is for previously resolved beforeLoad values to take precedence while a new beforeLoad is pending, consider placing __beforeLoadContext last:
    { ...parentMatchContext, ...match.__routeContext, ...match.__beforeLoadContext }
    This is not a blocker, but worth verifying with an explicit test for overlapping keys.
  • Safety: While these are usually objects in practice, using nullish fallbacks makes the spread robust and explicit.

Suggested change for both clarity and robustness:

-  const context = { ...match.__beforeLoadContext, ...parentMatchContext, ...match.__routeContext }
+  // Prefer explicit fallbacks to avoid spreading undefined/null.
+  // If you want previously computed beforeLoad values to win during pending,
+  // flip the order and spread __beforeLoadContext last instead.
+  const context = {
+    ...(match.__beforeLoadContext ?? {}),
+    ...(parentMatchContext ?? {}),
+    ...(match.__routeContext ?? {}),
+  }

If you do want __beforeLoadContext to win while pending, apply this alternative ordering:

-  const context = {
-    ...(match.__beforeLoadContext ?? {}),
-    ...(parentMatchContext ?? {}),
-    ...(match.__routeContext ?? {}),
-  }
+  const context = {
+    ...(parentMatchContext ?? {}),
+    ...(match.__routeContext ?? {}),
+    ...(match.__beforeLoadContext ?? {}),
+  }

Would you like me to add a focused test that asserts precedence when context() and beforeLoad() both define the same key (e.g., “title”) across preload→enter?

packages/react-router/tests/routeContext.test.tsx (1)

3129-3211: Solid repro for #4998; asserts no empty-context renders and correct preload→enter flow

This test effectively locks in the intended behavior:

  • preload beforeLoad runs and resolves first
  • navigation triggers beforeLoad again with cause: 'enter'
  • the route component never renders with {}; it renders first with the preloaded context and then updates.

Two small stability nits (optional):

  • Preload on focus can be async; consider waitFor for the initial “calledTimes(1)” assertion to reduce timing flakiness.
  • You can further harden the “no empty-context render” guarantee by asserting the component isn’t present before click.

Example refinements:

@@
-    fireEvent.focus(linkToFoo)
-    expect(beforeLoad).toHaveBeenCalledTimes(1)
+    fireEvent.focus(linkToFoo)
+    await waitFor(() => expect(beforeLoad).toHaveBeenCalledTimes(1))
@@
-    expect(select).not.toHaveBeenCalled()
+    expect(select).not.toHaveBeenCalled()
+    // Ensure the route component hasn't rendered yet during preload
+    expect(screen.queryByText('Foo index page')).toBeNull()

And add the missing import at the top of the file (outside this hunk):

import { waitFor } from '@testing-library/react'
📜 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 3f05c0b and 9a22133.

📒 Files selected for processing (2)
  • packages/react-router/tests/routeContext.test.tsx (1 hunks)
  • packages/router-core/src/load-matches.ts (1 hunks)
🔇 Additional comments (1)
packages/router-core/src/load-matches.ts (1)

372-372: Good call: seed context with prior __beforeLoadContext to avoid empty context during preload→enter

This directly addresses #4998 by ensuring the route gets a non-empty context (from the preload pass) while the enter pass beforeLoad is pending.

Copy link

nx-cloud bot commented Aug 20, 2025

View your CI Pipeline Execution ↗ for commit 02fb203

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

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

Copy link

pkg-pr-new bot commented Aug 20, 2025

More templates

@tanstack/arktype-adapter

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

@tanstack/directive-functions-plugin

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-plugin

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-plugin

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-server-functions-client

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

@tanstack/start-server-functions-fetcher

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

@tanstack/start-server-functions-server

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/zod-adapter

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

commit: 02fb203

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/react-router/tests/routeContext.test.tsx (5)

3129-3153: Rename spy to avoid ambiguity with the route’s beforeLoad prop

Using the identifier “beforeLoad” for the spy easily reads like recursion. Rename to a distinct name for clarity.

-  test("reproducer #4998 - on navigate (with preload), route component isn't rendered with undefined context if beforeLoad is pending", async () => {
-    const beforeLoad = vi.fn()
+  test("reproducer #4998 - on navigate (with preload), route component isn't rendered with undefined context if beforeLoad is pending", async () => {
+    const beforeLoadSpy = vi.fn()
@@
-    const fooRoute = createRoute({
+    const fooRoute = createRoute({
@@
-      beforeLoad: async (...args) => {
-        beforeLoad(...args)
+      beforeLoad: async (...args) => {
+        beforeLoadSpy(...args)

3165-3179: Stabilize timing: avoid relying on fixed sleep for preload completion

Relying on exact WAIT_TIME can be flaky under CI load. Wait for the expected condition instead.

-    fireEvent.focus(linkToFoo)
-    expect(beforeLoad).toHaveBeenCalledTimes(1)
-    expect(resolved).toBe(0)
-
-    await sleep(WAIT_TIME)
-
-    expect(beforeLoad).toHaveBeenCalledTimes(1)
+    fireEvent.focus(linkToFoo)
+    expect(beforeLoadSpy).toHaveBeenCalledTimes(1)
+    expect(resolved).toBe(0)
+
+    // Wait until the preload beforeLoad resolves
+    await waitFor(() => expect(resolved).toBe(1))
+
+    expect(beforeLoadSpy).toHaveBeenCalledTimes(1)
     expect(resolved).toBe(1)
-    expect(beforeLoad).toHaveBeenNthCalledWith(
+    expect(beforeLoadSpy).toHaveBeenNthCalledWith(
       1,
       expect.objectContaining({
         cause: 'preload',
         preload: true,
       }),
     )

Also add waitFor to the RTL import:

// at the top import
import { act, cleanup, configure, fireEvent, render, screen, waitFor } from '@testing-library/react'

3183-3196: Wait for the second beforeLoad call instead of asserting synchronously

The second invocation may not be observable immediately after click. Use waitFor to avoid race conditions.

-    fireEvent.click(linkToFoo)
-    expect(beforeLoad).toHaveBeenCalledTimes(2)
+    fireEvent.click(linkToFoo)
+    await waitFor(() => expect(beforeLoadSpy).toHaveBeenCalledTimes(2))
     expect(resolved).toBe(1)
@@
-    expect(beforeLoad).toHaveBeenCalledTimes(2)
-    expect(beforeLoad).toHaveBeenNthCalledWith(
+    expect(beforeLoadSpy).toHaveBeenCalledTimes(2)
+    expect(beforeLoadSpy).toHaveBeenNthCalledWith(
       2,
       expect.objectContaining({
         cause: 'enter',
         preload: false,
       }),
     )

3197-3204: Strengthen the “no empty context” assertion

This ensures every invocation received a non-empty context with “foo” defined, not just that one call matched.

-    expect(select).toHaveBeenNthCalledWith(
+    expect(select).toHaveBeenNthCalledWith(
       1,
       expect.objectContaining({
         foo: 1,
       }),
     )
-    expect(select).not.toHaveBeenCalledWith({})
+    // Every call so far should include foo
+    select.mock.calls.forEach(([ctx]) => {
+      expect(ctx).toEqual(expect.objectContaining({ foo: expect.any(Number) }))
+    })

3205-3218: Deflake: wait for the “enter” beforeLoad resolution by condition, not time

Mirror the preload-side change to keep the test robust under varying runtime speeds.

-    await sleep(WAIT_TIME)
-    expect(beforeLoad).toHaveBeenCalledTimes(2)
+    await waitFor(() => expect(resolved).toBe(2))
+    expect(beforeLoadSpy).toHaveBeenCalledTimes(2)
     expect(resolved).toBe(2)
@@
-    // the route component will be rendered multiple times, ensure it always has the context
-    expect(select).not.toHaveBeenCalledWith({})
+    // The route component renders multiple times; all should have non-empty context
+    select.mock.calls.forEach(([ctx]) => {
+      expect(ctx).toEqual(expect.objectContaining({ foo: expect.any(Number) }))
+    })
📜 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 9a22133 and 02fb203.

📒 Files selected for processing (2)
  • packages/react-router/tests/routeContext.test.tsx (1 hunks)
  • packages/router-core/src/load-matches.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router-core/src/load-matches.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/react-router/tests/routeContext.test.tsx (1)
packages/react-router/src/router.ts (1)
  • createRouter (80-82)
⏰ 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 (1)
packages/react-router/tests/routeContext.test.tsx (1)

3129-3218: Overall: great, targeted reproducer that asserts the correct context flow

The test captures the preload → enter interplay well and guards against the regression described in #4998. With the minor deflakes and naming tweak above, this should be rock-solid in CI.

@@ -369,7 +369,11 @@ const executeBeforeLoad = (
const parentMatchContext =
parentMatch?.context ?? inner.router.options.context ?? undefined

const context = { ...parentMatchContext, ...match.__routeContext }
const context = {
...match.__beforeLoadContext,
Copy link
Contributor

Choose a reason for hiding this comment

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

this is not correct IMO as this will feed the previous beforeLoad context into beforeLoad again.

Copy link
Author

@longzheng longzheng Aug 21, 2025

Choose a reason for hiding this comment

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

@schiller-manuel yes that is the intention, as otherwise the route component will be rendered while the beforeLoad is resolving without the beforeLoad context, which leads to the runtime undefined issues in #4998 .

Or are you suggesting that the route component shouldn't be rendered at all while the beforeLoad is resolving? If so I couldn't pinpoint when that behaviour was changed since I went back to over a year ago and it seems to have been always the case.

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.

Route component is rendered without route context from async beforeLoad with pendingComponent
2 participants