-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
base: main
Are you sure you want to change the base?
Conversation
…endered with undefined context if beforeLoad is pending
WalkthroughAdds 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
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 spreadsTwo 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 whenbeforeLoad
is pending, this could temporarily “hide” values derived during preload if keys overlap. If the intent is for previously resolvedbeforeLoad
values to take precedence while a newbeforeLoad
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()
andbeforeLoad()
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 flowThis test effectively locks in the intended behavior:
- preload
beforeLoad
runs and resolves first- navigation triggers
beforeLoad
again withcause: '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.
📒 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: seedcontext
with prior__beforeLoadContext
to avoid empty context during preload→enterThis directly addresses #4998 by ensuring the route gets a non-empty context (from the preload pass) while the enter pass
beforeLoad
is pending.
View your CI Pipeline Execution ↗ for commit 02fb203
☁️ Nx Cloud last updated this comment at |
More templates
@tanstack/arktype-adapter
@tanstack/directive-functions-plugin
@tanstack/eslint-plugin-router
@tanstack/history
@tanstack/react-router
@tanstack/react-router-devtools
@tanstack/react-router-ssr-query
@tanstack/react-start
@tanstack/react-start-client
@tanstack/react-start-plugin
@tanstack/react-start-server
@tanstack/router-cli
@tanstack/router-core
@tanstack/router-devtools
@tanstack/router-devtools-core
@tanstack/router-generator
@tanstack/router-plugin
@tanstack/router-ssr-query-core
@tanstack/router-utils
@tanstack/router-vite-plugin
@tanstack/server-functions-plugin
@tanstack/solid-router
@tanstack/solid-router-devtools
@tanstack/solid-start
@tanstack/solid-start-client
@tanstack/solid-start-plugin
@tanstack/solid-start-server
@tanstack/start-client-core
@tanstack/start-plugin-core
@tanstack/start-server-core
@tanstack/start-server-functions-client
@tanstack/start-server-functions-fetcher
@tanstack/start-server-functions-server
@tanstack/start-storage-context
@tanstack/valibot-adapter
@tanstack/virtual-file-routes
@tanstack/zod-adapter
commit: |
There was a problem hiding this 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 propUsing 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 completionRelying 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 synchronouslyThe 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” assertionThis 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 timeMirror 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.
📒 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 flowThe 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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Fixes #4998
Summary by CodeRabbit
Bug Fixes
Tests