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
17 changes: 17 additions & 0 deletions e2e/react-start/context-bridge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
node_modules
package-lock.json
yarn.lock

.DS_Store
.cache
.env
.vercel
.output

/dist/
/build/

/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
37 changes: 37 additions & 0 deletions e2e/react-start/context-bridge/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "tanstack-react-start-e2e-context-bridge",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"dev:e2e": "vite dev",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-router-devtools": "workspace:^",
"@tanstack/react-start": "workspace:^",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
Comment on lines +14 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use workspace:* for internal TanStack dependencies.

🔧 Suggested update
   "dependencies": {
-    "@tanstack/react-router": "workspace:^",
-    "@tanstack/react-router-devtools": "workspace:^",
-    "@tanstack/react-start": "workspace:^",
+    "@tanstack/react-router": "workspace:*",
+    "@tanstack/react-router-devtools": "workspace:*",
+    "@tanstack/react-start": "workspace:*",
     "express": "^5.1.0",
     "http-proxy-middleware": "^3.0.5",
     "react": "^19.0.0",
     "react-dom": "^19.0.0"
   },
   "devDependencies": {
     "@playwright/test": "^1.50.1",
     "@tailwindcss/vite": "^4.1.18",
-    "@tanstack/router-e2e-utils": "workspace:^",
+    "@tanstack/router-e2e-utils": "workspace:*",

As per coding guidelines: Use workspace protocol workspace:* for internal dependencies in package.json files.

📝 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
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-router-devtools": "workspace:^",
"@tanstack/react-start": "workspace:^",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"dependencies": {
"@tanstack/react-router": "workspace:*",
"@tanstack/react-router-devtools": "workspace:*",
"@tanstack/react-start": "workspace:*",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/router-e2e-utils": "workspace:*",
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
🤖 Prompt for AI Agents
In `@e2e/react-start/context-bridge/package.json` around lines 14 - 30, Update the
package.json dependency specifiers for internal TanStack packages to use the
workspace protocol wildcard: replace "workspace:^" with "workspace:*" for the
packages named "@tanstack/react-router", "@tanstack/react-router-devtools",
"@tanstack/react-start", and "@tanstack/router-e2e-utils" so they use
workspace:* instead of workspace:^.

"srvx": "^0.10.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.7.2",
"vite": "^7.3.1",
"vite-tsconfig-paths": "^5.1.4"
}
}
29 changes: 29 additions & 0 deletions e2e/react-start/context-bridge/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

export 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_SERVER_PORT=${PORT} pnpm build && PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm start`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
})
87 changes: 87 additions & 0 deletions e2e/react-start/context-bridge/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as NextRouteImport } from './routes/next'
import { Route as IndexRouteImport } from './routes/index'

const NextRoute = NextRouteImport.update({
id: '/next',
path: '/next',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/next': typeof NextRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/next': typeof NextRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/next': typeof NextRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/next'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/next'
id: '__root__' | '/' | '/next'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
NextRoute: typeof NextRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/next': {
id: '/next'
path: '/next'
fullPath: '/next'
preLoaderRoute: typeof NextRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NextRoute: NextRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

import type { getRouter } from './router.tsx'
import type { startInstance } from './start.ts'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
config: Awaited<ReturnType<typeof startInstance.getOptions>>
}
}
34 changes: 34 additions & 0 deletions e2e/react-start/context-bridge/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createRouter } from '@tanstack/react-router'
import { createStartContextBridge } from '@tanstack/react-start'
import { routeTree } from './routeTree.gen'

export type RouterContext = {
a: string
c: string
shared: string
static: string
}

const startContextBridge = createStartContextBridge({
select: (ctx) => ({
a: ctx.a,
c: ctx.c,
shared: ctx.shared,
}),
})

export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
defaultPreload: 'intent',
context: {
...startContextBridge.get(),
static: 'static-value',
},
})

startContextBridge.setup(router)

return router
}
62 changes: 62 additions & 0 deletions e2e/react-start/context-bridge/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/// <reference types="vite/client" />
import * as React from 'react'
import {
HeadContent,
Link,
Scripts,
createRootRouteWithContext,
} from '@tanstack/react-router'
import type { RouterContext } from '~/router'
import appCss from '~/styles/app.css?url'

export const Route = createRootRouteWithContext<RouterContext>()({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
links: [{ rel: 'stylesheet', href: appCss }],
}),
shellComponent: RootDocument,
})

const RouterDevtools =
process.env.NODE_ENV === 'production'
? () => null
: React.lazy(() =>
import('@tanstack/react-router-devtools').then((res) => ({
default: res.TanStackRouterDevtools,
})),
)

function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<div className="p-2 flex gap-3 text-lg">
<Link
to="/"
activeOptions={{ exact: true }}
activeProps={{ className: 'font-bold' }}
>
Home
</Link>
<Link
to="/next"
activeOptions={{ exact: true }}
activeProps={{ className: 'font-bold' }}
>
Next
</Link>
</div>
<hr />
{children}
<RouterDevtools position="bottom-right" />
<Scripts />
Comment on lines +23 to +58
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

React.lazy Suspense boundary requirement React 18 19 runtime error

💡 Result:

In React 18 and React 19, a React.lazy() component must be rendered under an ancestor <Suspense> boundary because rendering a lazy component while its code is still loading “suspends” (it throws a Promise). React then walks up the rendered tree looking for the nearest <Suspense> to show its fallback; if it can’t find one, you’ll get a runtime failure/blank screen. [1]

Key “boundary requirement” gotchas that commonly cause runtime errors:

  • The <Suspense> must be above the lazy component in the rendered tree. A <Suspense> inside the component that suspends won’t help, because React can’t reach it when that component suspends during its own render. [2]
  • If you see A component suspended while responding to synchronous input… in React 18+, it usually means a user-driven synchronous update (navigation, typing, click) caused something to suspend (often a lazy route or Suspense-based data). Typical fixes are:
    • move/add a higher Suspense boundary (e.g., wrap your router / route outlet), and/or
    • wrap the update that triggers the suspend in startTransition. [3]

Minimal correct pattern

import { Suspense, lazy } from "react";

const Page = lazy(() => import("./Page"));

export function App() {
  return (
    <Suspense fallback={<div>Loading…</div>}>
      <Page />
    </Suspense>
  );
}

Sources: React lazy docs (v19.2) [1], explanation of boundary placement (React walks up to nearest <Suspense>) [2], React 18 runtime error (“suspended while responding to synchronous input”) discussion and fixes [3].

[1] (react.dev)
[2] (stackoverflow.com)
[3] (stackoverflow.com)

Citations:


Wrap the lazy devtools in a Suspense boundary.
Rendering a React.lazy component without Suspense will throw at runtime. The <Suspense> boundary must be placed above the lazy component in the rendered tree for React to properly handle code-splitting and suspense.

✅ Suggested fix
-        <RouterDevtools position="bottom-right" />
+        <React.Suspense fallback={null}>
+          <RouterDevtools position="bottom-right" />
+        </React.Suspense>
📝 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
const RouterDevtools =
process.env.NODE_ENV === 'production'
? () => null
: React.lazy(() =>
import('@tanstack/react-router-devtools').then((res) => ({
default: res.TanStackRouterDevtools,
})),
)
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<div className="p-2 flex gap-3 text-lg">
<Link
to="/"
activeOptions={{ exact: true }}
activeProps={{ className: 'font-bold' }}
>
Home
</Link>
<Link
to="/next"
activeOptions={{ exact: true }}
activeProps={{ className: 'font-bold' }}
>
Next
</Link>
</div>
<hr />
{children}
<RouterDevtools position="bottom-right" />
<Scripts />
const RouterDevtools =
process.env.NODE_ENV === 'production'
? () => null
: React.lazy(() =>
import('@tanstack/react-router-devtools').then((res) => ({
default: res.TanStackRouterDevtools,
})),
)
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<div className="p-2 flex gap-3 text-lg">
<Link
to="/"
activeOptions={{ exact: true }}
activeProps={{ className: 'font-bold' }}
>
Home
</Link>
<Link
to="/next"
activeOptions={{ exact: true }}
activeProps={{ className: 'font-bold' }}
>
Next
</Link>
</div>
<hr />
{children}
<React.Suspense fallback={null}>
<RouterDevtools position="bottom-right" />
</React.Suspense>
<Scripts />
🤖 Prompt for AI Agents
In `@e2e/react-start/context-bridge/src/routes/__root.tsx` around lines 23 - 58,
RootDocument renders the lazily-loaded RouterDevtools (RouterDevtools) directly
which will throw without a Suspense boundary; update RootDocument to wrap the
RouterDevtools invocation in a React.Suspense boundary (with a lightweight
fallback such as null or a small loader) so the lazy import resolves correctly,
and ensure React.Suspense is imported/available in the file; keep the existing
production-guarded RouterDevtools definition unchanged.

</body>
</html>
)
}
78 changes: 78 additions & 0 deletions e2e/react-start/context-bridge/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Link, createFileRoute, useRouter } from '@tanstack/react-router'
import { getGlobalStartContext } from '@tanstack/react-start'
import { sortJson } from '~/utils/sortJson'

export const Route = createFileRoute('/')({
beforeLoad: async ({ context }) => {
const globalStartContext = getGlobalStartContext()
return {
serverContext: context,
globalStartContext,
}
},
component: Home,
})

function Home() {
const router = useRouter()
const { serverContext, globalStartContext } = Route.useRouteContext()
const routerCtx = router.options.context

return (
<div className="p-8 space-y-6">
<h1 className="font-bold text-lg">Start context bridge</h1>

<div>
<div className="font-semibold">
Bridged context (router.options.context)
</div>
<pre
data-testid="bridged-context"
className="bg-gray-100 p-2 rounded text-black"
>
{JSON.stringify(
{
context: sortJson(routerCtx),
},
null,
2,
)}
</pre>
</div>

<div>
<div className="font-semibold">Router context (from beforeLoad)</div>
<pre
data-testid="server-context"
className="bg-gray-100 p-2 rounded text-black"
>
{JSON.stringify(sortJson(serverContext), null, 2)}
</pre>
</div>

<div>
<div className="font-semibold">Global start context</div>
<pre
data-testid="global-start-context"
className="bg-gray-100 p-2 rounded text-black"
>
{JSON.stringify(sortJson(globalStartContext), null, 2)}
</pre>
</div>

<div className="flex items-center gap-3">
<Link data-testid="to-next" to="/next" className="underline">
Go to /next
</Link>
<button
type="button"
className="px-2 py-1 border rounded"
data-testid="invalidate"
onClick={() => router.invalidate()}
>
Invalidate
</button>
</div>
</div>
)
}
34 changes: 34 additions & 0 deletions e2e/react-start/context-bridge/src/routes/next.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { sortJson } from '~/utils/sortJson'

export const Route = createFileRoute('/next')({
component: Next,
})

function Next() {
const router = useRouter()

return (
<div className="p-8 space-y-6">
<h1 className="font-bold text-lg">Next</h1>

<div>
<div className="font-semibold">
Bridged context (router.options.context)
</div>
<pre
data-testid="bridged-context-next"
className="bg-gray-100 p-2 rounded text-black"
>
{JSON.stringify(
{
context: sortJson(router.options.context),
},
null,
2,
)}
</pre>
</div>
</div>
)
}
Loading
Loading