Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/violet-poets-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/router-core': patch
---

Fix context values from a parent route's `beforeLoad` not being propagated to sub-routes in several code paths: while a sub-route's loader reloads in the background, when re-entering a route whose background reload is still in flight, and in a sub-route's error state when its `beforeLoad` throws (the merged context is now committed together with the error status for the errorComponent to consume).

Redirects no longer use a renderable `RouteMatch.status`; `RouteMatch.status` is now `'pending' | 'success' | 'error' | 'notFound'`. Abandoned pending, redirected, or failed matches are dropped from cache and their pending promises are settled so stale suspense work cannot keep rendering suspended.
2 changes: 1 addition & 1 deletion docs/router/api/router/RouteMatchType.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface RouteMatch {
routeId: string
pathname: string
params: Route['allParams']
status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
status: 'pending' | 'success' | 'error' | 'notFound'
isFetching: false | 'beforeLoad' | 'loader'
showPending: boolean
error: unknown
Expand Down
12 changes: 12 additions & 0 deletions e2e/react-router/issue-7120/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Issue 7120</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions e2e/react-router/issue-7120/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "tanstack-router-e2e-react-issue-7120",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"dev:e2e": "vite",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "vite",
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
Comment on lines +14 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align internal workspace dependency ranges with repo guideline.

Line 14 and Line 20 use workspace:^ for internal packages, but this PR’s package guideline requires workspace:* in package.json files. Please switch both entries (or explicitly document an e2e exception in the guideline to avoid drift).

Suggested patch
-    "`@tanstack/react-router`": "workspace:^",
+    "`@tanstack/react-router`": "workspace:*",
@@
-    "`@tanstack/router-e2e-utils`": "workspace:^",
+    "`@tanstack/router-e2e-utils`": "workspace:*",

As per coding guidelines, internal dependencies in **/package.json must use the workspace protocol workspace:*.

📝 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
"@tanstack/react-router": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"`@tanstack/react-router`": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"`@playwright/test`": "^1.50.1",
"`@tanstack/router-e2e-utils`": "workspace:*",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-router/issue-7120/package.json` around lines 14 - 20, The
package.json file uses workspace:^ for internal workspace packages
(`@tanstack/react-router` and `@tanstack/router-e2e-utils`) but the repository
guideline requires workspace:* for all internal dependencies. Change both
`@tanstack/react-router` and `@tanstack/router-e2e-utils` entries from workspace:^
to workspace:* to align with the established guideline and prevent dependency
drift.

Source: Coding guidelines

"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^6.0.1",
"vite": "^8.0.14"
}
}
27 changes: 27 additions & 0 deletions e2e/react-router/issue-7120/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`

export default defineConfig({
testDir: './tests',
workers: 1,
reporter: [['line']],
use: {
baseURL,
},
webServer: {
command: `VITE_NODE_ENV="test" VITE_SERVER_PORT=${PORT} pnpm build && pnpm preview --port ${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
72 changes: 72 additions & 0 deletions e2e/react-router/issue-7120/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import ReactDOM from 'react-dom/client'
import {
Outlet,
RouterProvider,
createRootRoute,
createRoute,
createRouter,
redirect,
} from '@tanstack/react-router'

const posts = [
{
id: '1',
title: 'sunt aut facere repellat provident occaecati',
},
]

const rootRoute = createRootRoute({
component: RootComponent,
pendingMs: 0,
pendingComponent: () => {
;(globalThis as any).__pendingSeen = true
return <div data-testid="root-pending">loading</div>
},
beforeLoad: async ({ matches }) => {
if (matches.find((match) => match.routeId === '/posts')) {
return
}

await new Promise((resolve) => setTimeout(resolve, 1000))
throw redirect({ to: '/posts' })
},
})

function RootComponent() {
return <Outlet />
}

const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div>Home</div>,
})

const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
loader: async () => {
await new Promise((resolve) => setTimeout(resolve, 10))
return posts
},
}).lazy(() => import('./posts.lazy').then((d) => d.Route))

const routeTree = rootRoute.addChildren([indexRoute, postsRoute])

const router = createRouter({
routeTree,
defaultViewTransition: true,
})

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

const rootElement = document.getElementById('app')!

if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(<RouterProvider router={router} />)
}
25 changes: 25 additions & 0 deletions e2e/react-router/issue-7120/src/posts.lazy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Link, createLazyRoute } from '@tanstack/react-router'

export const Route = createLazyRoute('/posts')({
component: PostsComponent,
})

function PostsComponent() {
const posts = Route.useLoaderData()

return (
<div>
<ul>
{posts.map((post) => {
return (
<li key={post.id}>
<Link to="/posts">
<div>{post.title.substring(0, 20)}</div>
</Link>
</li>
)
})}
</ul>
</div>
)
}
28 changes: 28 additions & 0 deletions e2e/react-router/issue-7120/tests/issue-7120.repro.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect, test } from '@playwright/test'

test('root beforeLoad redirect does not blank when pending UI and view transitions are enabled (https://github.com/TanStack/router/issues/7120)', async ({
page,
}) => {
const pageErrors: Array<string> = []
const consoleErrors: Array<string> = []

page.on('pageerror', (error) => {
pageErrors.push(error.message)
})
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(message.text())
}
})

await page.goto('/')

await expect(page).toHaveURL(/\/posts$/)
await expect(page.getByText('sunt aut facere repe')).toBeVisible()
await expect
.poll(() => page.evaluate(() => (globalThis as any).__pendingSeen))
.toBe(true)
await expect(page.getByTestId('root-pending')).not.toBeVisible()
expect(pageErrors).toEqual([])
expect(consoleErrors).toEqual([])
})
15 changes: 15 additions & 0 deletions e2e/react-router/issue-7120/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"target": "ESNext",
"moduleResolution": "Bundler",
"module": "ESNext",
"resolveJsonModule": true,
"allowJs": true,
"skipLibCheck": true,
"types": ["vite/client"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include Node types for Playwright config type-checking.

Line 12 restricts global types to vite/client, but playwright.config.ts uses process.env (Line 18 there). With tsc --noEmit in Line 8 of package.json, this can fail type-checking due to missing Node globals.

Suggested patch
-    "types": ["vite/client"]
+    "types": ["vite/client", "node"]
📝 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
"types": ["vite/client"]
"types": ["vite/client", "node"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-router/issue-7120/tsconfig.json` at line 12, The types
configuration in tsconfig.json at the "types" field currently only includes
vite/client, which does not provide Node global type definitions. Since
playwright.config.ts uses process.env, the Node types must be added to the types
array to resolve the process global and satisfy type-checking when tsc --noEmit
is run. Add "node" to the types array alongside the existing "vite/client"
entry.

},
"exclude": ["node_modules", "dist"]
}
6 changes: 6 additions & 0 deletions e2e/react-router/issue-7120/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
plugins: [react()],
})
10 changes: 10 additions & 0 deletions e2e/react-router/issue-7457/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Issue 7457</title>
<script type="module" src="/src/main.tsx"></script>
</head>
<body id="root" class="flex h-svh flex-col"></body>
</html>
27 changes: 27 additions & 0 deletions e2e/react-router/issue-7457/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "tanstack-router-e2e-react-issue-7457",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"dev:e2e": "vite",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "vite",
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/router-plugin": "workspace:^",
Comment on lines +14 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify workspace protocol usage in this package
rg -n '"`@tanstack/`(react-router|router-plugin|router-e2e-utils)"\s*:\s*"workspace:\^"' \
  e2e/react-router/issue-7457/package.json

Repository: TanStack/router

Length of output: 208


Use workspace:* for internal dependencies in this package.

Lines 14–15 and 21 use workspace:^, but the coding guideline for **/package.json requires workspace:* for internal dependencies.

Suggested patch
-    "`@tanstack/react-router`": "workspace:^",
-    "`@tanstack/router-plugin`": "workspace:^",
+    "`@tanstack/react-router`": "workspace:*",
+    "`@tanstack/router-plugin`": "workspace:*",
@@
-    "`@tanstack/router-e2e-utils`": "workspace:^",
+    "`@tanstack/router-e2e-utils`": "workspace:*",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-router/issue-7457/package.json` around lines 14 - 15, Replace the
version specifiers for internal dependencies in the package.json file from
workspace:^ to workspace:* to comply with the coding guideline for internal
dependency versioning. Specifically, change the version specifier for
`@tanstack/react-router` on line 14, `@tanstack/router-plugin` on line 15, and the
internal dependency on line 21 from workspace:^ to workspace:*.

Source: Coding guidelines

"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^6.0.1",
"vite": "^8.0.14"
}
}
27 changes: 27 additions & 0 deletions e2e/react-router/issue-7457/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`

export default defineConfig({
testDir: './tests',
workers: 1,
reporter: [['line']],
use: {
baseURL,
},
webServer: {
command: `VITE_NODE_ENV="test" VITE_SERVER_PORT=${PORT} pnpm build && pnpm preview --port ${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
28 changes: 28 additions & 0 deletions e2e/react-router/issue-7457/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

const router = createRouter({
routeTree,
defaultPendingComponent: DefaultPendingComponent,
defaultPreloadStaleTime: 0,
})

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

function DefaultPendingComponent() {
;(globalThis as any).__pendingSeen = true

return <div data-testid="app-pending">loading</div>
}

createRoot(document.body).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
Loading
Loading