Skip to content

Conversation

TkDodo
Copy link
Contributor

@TkDodo TkDodo commented Oct 16, 2025

fixes #2317

Summary by CodeRabbit

  • New Features

    • Route-level pending UI: root routes can now specify their own loading placeholder.
  • Bug Fixes

    • Pending-component selection now prefers a route's pending UI and falls back to the router default for consistent loading visuals.
  • Tests

    • Added tests verifying route/root pending behavior and router-level fallback during async route loading.

Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

Resolve the root route's pendingComponent (from router.routesById[rootRouteId]) before falling back to the router's defaultPendingComponent, and use that resolved component (if any) as the Suspense fallback so the root route's pending UI remains mounted while its loader runs.

Changes

Cohort / File(s) Summary
Matches: pending resolution (React & Solid)
packages/react-router/src/Matches.tsx, packages/solid-router/src/Matches.tsx
Import rootRouteId and AnyRoute from @tanstack/router-core; derive the root route via router.routesById[rootRouteId]; compute `PendingComponent = rootRoute?.options.pendingComponent
Tests: pending behavior for root route
packages/react-router/tests/Matches.test.tsx, packages/solid-router/tests/Matches.test.tsx
Add tests that define a root route with pendingComponent and a delayed loader, configure the router with defaultPendingMs/defaultPendingComponent, and assert the root pending UI appears before the root content.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Router
  participant Root as __root__
  participant Child

  rect rgba(135,206,250,0.12)
  Note over Router,Root: Resolve PendingComponent
  Router->>Root: lookup router.routesById[rootRouteId]
  Root-->>Router: root.options.pendingComponent (or undefined)
  Router->>Router: PendingComponent = root.pendingComponent || defaultPendingComponent
  end

  App->>Router: mount / navigate
  Router->>Root: run loader (async)
  alt PendingComponent exists
    Router->>App: render <PendingComponent/> as Suspense fallback
  else
    Router->>App: render null fallback
  end
  Root-->>Router: loader resolves
  Router->>Child: render child/root content
  Router->>App: update UI with resolved content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • birkskyum
  • brenelz
  • schiller-manuel

Poem

🐰 I nudge the root, I hold the light,

A pending hop through loader's night,
I stay till leaves of content show,
Then joyful thumps — away we go! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "fix(react-router,solid-router): top-level Matches should use pendingComponent on root route" directly and clearly reflects the main change in the pull request. The changes in both packages modify the Matches component to check for and prioritize the pendingComponent defined on the root route before falling back to the router's defaultPendingComponent. The title is concise, specific, and accurately summarizes the primary objective of the changeset without vague language or unnecessary details.
Linked Issues Check ✅ Passed The code changes directly address the requirements from issue #2317. The PR modifies the top-level Matches component in both react-router and solid-router to extract and use the pendingComponent from the root route (via rootRoute.options.pendingComponent) in addition to the default fallback behavior. The test cases validate that when a pendingComponent is defined on the root route, it renders and persists while the root route's loader is executing, which matches the expected behavior described in the issue. This implementation resolves the bug where pending components on the root route were not remaining mounted during loader execution.
Out of Scope Changes Check ✅ Passed All changes in the pull request are directly scoped to the stated objective of enabling the top-level Matches component to use pendingComponent from the root route. The modifications to both Matches.tsx files in react-router and solid-router are focused updates that introduce root route lookup logic and incorporate the root route's pendingComponent into the rendering pipeline. The test additions validate this specific behavior. No unrelated refactoring, style changes, or feature additions are present in the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/fix/pending-root-route

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

Copy link

nx-cloud bot commented Oct 16, 2025

View your CI Pipeline Execution ↗ for commit 6fc6e2b

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

☁️ Nx Cloud last updated this comment at 2025-10-17 06:46:19 UTC

Copy link

pkg-pr-new bot commented Oct 16, 2025

More templates

@tanstack/arktype-adapter

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

@tanstack/directive-functions-plugin

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/zod-adapter

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

commit: 6fc6e2b

@TkDodo TkDodo marked this pull request as ready for review October 16, 2025 09:04
Copy link
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

🧹 Nitpick comments (1)
packages/solid-router/src/Matches.tsx (1)

49-52: Consider simplifying rootRoute to a direct assignment.

The function wrapper () => router.routesById['__root__'] provides no reactivity benefit here since it's only called once when computing PendingComponent at the top level. Direct assignment would be clearer and consistent with the React implementation.

Apply this diff to simplify:

-  const rootRoute: () => AnyRoute = () => router.routesById['__root__']
+  const rootRoute: AnyRoute = router.routesById['__root__']
   const PendingComponent =
-    rootRoute().options.pendingComponent ??
+    rootRoute.options.pendingComponent ??
     router.options.defaultPendingComponent
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4303a8a and dd78c82.

📒 Files selected for processing (2)
  • packages/react-router/src/Matches.tsx (2 hunks)
  • packages/solid-router/src/Matches.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • packages/solid-router/src/Matches.tsx
  • packages/react-router/src/Matches.tsx
packages/{react-router,solid-router}/**

📄 CodeRabbit inference engine (AGENTS.md)

Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/

Files:

  • packages/solid-router/src/Matches.tsx
  • packages/react-router/src/Matches.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.598Z
Learning: Applies to packages/{react-router,solid-router}/** : Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/
📚 Learning: 2025-09-23T17:36:12.598Z
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.598Z
Learning: Applies to packages/{react-router,solid-router}/** : Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/

Applied to files:

  • packages/solid-router/src/Matches.tsx
🧬 Code graph analysis (2)
packages/solid-router/src/Matches.tsx (1)
packages/router-core/src/route.ts (1)
  • AnyRoute (778-797)
packages/react-router/src/Matches.tsx (1)
packages/router-core/src/route.ts (1)
  • AnyRoute (778-797)
🔇 Additional comments (3)
packages/react-router/src/Matches.tsx (2)

15-15: LGTM: Type import added correctly.

The AnyRoute type import is necessary for typing the root route.


45-50: LGTM: Root route pending component resolution implemented correctly.

The implementation correctly resolves the pending component from the root route's options before falling back to the router's default pending component. This fixes the issue where the pending component defined on __root__ was not being used.

The nullish coalescing operator ensures proper fallback behavior, and the pendingElement correctly renders the component or null.

packages/solid-router/src/Matches.tsx (1)

59-59: LGTM: Pending component resolution implemented correctly.

The fallback correctly uses the resolved PendingComponent, which now properly prioritizes the root route's pending component before falling back to the router's default. This fixes the issue where the pending component defined on __root__ was not being displayed.


export function Matches() {
const router = useRouter()
const rootRoute: AnyRoute = router.routesById['__root__']
Copy link
Contributor Author

Choose a reason for hiding this comment

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

do we need to be defensive here and type this as AnyRoute | undefined. I think a __root__ route must always exist ...

Copy link
Contributor

Choose a reason for hiding this comment

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

please use rootRouteId from @tanstack/router-core instead of __root__

Copy link
Member

Choose a reason for hiding this comment

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

and to answer your question, the answer is probably not, since a root route must always be present, but if it helps silence typescript then a ! assertion or a throw Error should be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done: 74a3835

Copy link
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: 0

🧹 Nitpick comments (2)
packages/solid-router/tests/Matches.test.tsx (1)

225-247: LGTM! Test correctly verifies root route's pendingComponent is displayed.

The test successfully validates that the root route's pendingComponent is shown during the loader execution and that the index route renders after loading completes.

Optional enhancement: Consider adding an assertion to verify the pending component is no longer present after loading completes, and that the defaultPendingComponent was never used:

  expect(await rendered.findByText('root pending...')).toBeInTheDocument()
  expect(await rendered.findByText('index route')).toBeInTheDocument()
+ expect(rendered.queryByText('root pending...')).not.toBeInTheDocument()
+ expect(rendered.queryByText('default pending...')).not.toBeInTheDocument()

This would make the test more explicit about the expected behavior and provide better coverage of the pending component lifecycle.

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

125-147: LGTM! Test correctly verifies root route's pendingComponent is displayed.

The test successfully validates that the root route's pendingComponent is shown during the loader execution and that the index route renders after loading completes.

Optional enhancement: Consider adding an assertion to verify the pending component is no longer present after loading completes, and that the defaultPendingComponent was never used:

  expect(await rendered.findByText('root pending...')).toBeInTheDocument()
  expect(await rendered.findByText('index route')).toBeInTheDocument()
+ expect(rendered.queryByText('root pending...')).not.toBeInTheDocument()
+ expect(rendered.queryByText('default pending...')).not.toBeInTheDocument()

This would make the test more explicit about the expected behavior and provide better coverage of the pending component lifecycle.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 955e61a and 051390a.

📒 Files selected for processing (2)
  • packages/react-router/tests/Matches.test.tsx (1 hunks)
  • packages/solid-router/tests/Matches.test.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • packages/react-router/tests/Matches.test.tsx
  • packages/solid-router/tests/Matches.test.tsx
packages/{react-router,solid-router}/**

📄 CodeRabbit inference engine (AGENTS.md)

Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/

Files:

  • packages/react-router/tests/Matches.test.tsx
  • packages/solid-router/tests/Matches.test.tsx
🧬 Code graph analysis (1)
packages/solid-router/tests/Matches.test.tsx (1)
packages/solid-router/src/RouterProvider.tsx (1)
  • RouterProvider (44-53)
⏰ 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

Copy link
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: 0

🧹 Nitpick comments (1)
packages/react-router/tests/Matches.test.tsx (1)

125-143: Consider adding an assertion to verify defaultPendingComponent is not rendered.

The test correctly verifies that the root route's pendingComponent is shown, but it would be more comprehensive to also assert that the router's defaultPendingComponent ("default pending...") is NOT shown. This would explicitly validate the precedence behavior being fixed.

Add this assertion after rendering but before the first findByText:

   const rendered = render(<RouterProvider router={router} />)

+  expect(rendered.queryByText('default pending...')).not.toBeInTheDocument()
   expect(await rendered.findByText('root pending...')).toBeInTheDocument()
   expect(await rendered.findByText('root content')).toBeInTheDocument()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 051390a and 13d45c1.

📒 Files selected for processing (2)
  • packages/react-router/tests/Matches.test.tsx (1 hunks)
  • packages/solid-router/tests/Matches.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/solid-router/tests/Matches.test.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • packages/react-router/tests/Matches.test.tsx
packages/{react-router,solid-router}/**

📄 CodeRabbit inference engine (AGENTS.md)

Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/

Files:

  • packages/react-router/tests/Matches.test.tsx
⏰ 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


test('should show pendingComponent of root route', async () => {
const root = createRootRoute({
pendingComponent: () => <div>root pending...</div>,
Copy link
Contributor

Choose a reason for hiding this comment

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

please use data-testid and getByTestId or similar methods instead of looking for textual matches

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done: 6fc6e2b

@SeanCassiere SeanCassiere changed the title fix: the top level Matches component should use the pendingComponent defined on the __root__ route fix(react-router,solid-router): top-level Matches should use pendingComponent on root route Oct 16, 2025
Copy link
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: 0

🧹 Nitpick comments (1)
packages/solid-router/src/Matches.tsx (1)

49-52: Consider simplifying rootRoute to a direct reference.

The rootRoute is defined as a function but only called once to derive PendingComponent. Since PendingComponent is not wrapped in a reactive primitive (like createMemo), it won't update reactively anyway. Additionally, consider adding optional chaining for defensive coding in case the root route is unexpectedly missing.

Apply this diff to simplify:

-  const rootRoute: () => AnyRoute = () => router.routesById[rootRouteId]
-  const PendingComponent =
-    rootRoute().options.pendingComponent ??
-    router.options.defaultPendingComponent
+  const rootRoute = router.routesById[rootRouteId]
+  const PendingComponent =
+    rootRoute?.options.pendingComponent ??
+    router.options.defaultPendingComponent
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13d45c1 and 6fc6e2b.

📒 Files selected for processing (4)
  • packages/react-router/src/Matches.tsx (3 hunks)
  • packages/react-router/tests/Matches.test.tsx (1 hunks)
  • packages/solid-router/src/Matches.tsx (3 hunks)
  • packages/solid-router/tests/Matches.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/react-router/src/Matches.tsx
  • packages/react-router/tests/Matches.test.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • packages/solid-router/tests/Matches.test.tsx
  • packages/solid-router/src/Matches.tsx
packages/{react-router,solid-router}/**

📄 CodeRabbit inference engine (AGENTS.md)

Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/

Files:

  • packages/solid-router/tests/Matches.test.tsx
  • packages/solid-router/src/Matches.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-23T17:36:12.598Z
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.598Z
Learning: Applies to packages/{react-router,solid-router}/** : Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/

Applied to files:

  • packages/solid-router/src/Matches.tsx
🧬 Code graph analysis (2)
packages/solid-router/tests/Matches.test.tsx (1)
packages/solid-router/src/RouterProvider.tsx (1)
  • RouterProvider (44-53)
packages/solid-router/src/Matches.tsx (1)
packages/router-core/src/route.ts (1)
  • AnyRoute (778-797)
⏰ 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: Preview
  • GitHub Check: Test
🔇 Additional comments (3)
packages/solid-router/src/Matches.tsx (2)

3-3: LGTM! Imports support the root route pending component resolution.

The new imports of rootRouteId and AnyRoute are necessary for accessing and typing the root route to resolve its pendingComponent.

Also applies to: 12-12


49-59: Excellent fix! Root route's pendingComponent now renders correctly.

The implementation correctly prioritizes the root route's pendingComponent over the router's defaultPendingComponent and uses it as the Suspense fallback. This resolves the issue where the root route's pending component was unmounting prematurely.

packages/solid-router/tests/Matches.test.tsx (1)

225-244: LGTM! Test validates root route pendingComponent behavior.

The test correctly verifies that:

  1. A pendingComponent can be specified on the root route
  2. The pending component renders while the root loader executes
  3. The root content appears after the loader completes

The test structure properly exercises the fix for issue #2317.

@schiller-manuel schiller-manuel merged commit b0d0b25 into main Oct 17, 2025
6 checks passed
@schiller-manuel schiller-manuel deleted the feature/fix/pending-root-route branch October 17, 2025 17:19
@coderabbitai coderabbitai bot mentioned this pull request Oct 17, 2025
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.

Pending component doesn't work on root route

3 participants