diff --git a/.changeset/fresh-ducks-hydrate.md b/.changeset/fresh-ducks-hydrate.md new file mode 100644 index 0000000000..27c897932d --- /dev/null +++ b/.changeset/fresh-ducks-hydrate.md @@ -0,0 +1,5 @@ +--- +'@tanstack/react-router': patch +--- + +Avoid wrapping root components that may render the HTML document in a Suspense boundary during SSR and hydration. diff --git a/e2e/react-start/root-document-hydration/.gitignore b/e2e/react-start/root-document-hydration/.gitignore new file mode 100644 index 0000000000..229709a89d --- /dev/null +++ b/e2e/react-start/root-document-hydration/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +test-results +playwright-report +port*.txt diff --git a/e2e/react-start/root-document-hydration/.prettierignore b/e2e/react-start/root-document-hydration/.prettierignore new file mode 100644 index 0000000000..083bdb7c4c --- /dev/null +++ b/e2e/react-start/root-document-hydration/.prettierignore @@ -0,0 +1 @@ +src/routeTree.gen.ts diff --git a/e2e/react-start/root-document-hydration/package.json b/e2e/react-start/root-document-hydration/package.json new file mode 100644 index 0000000000..d69ddfc0bf --- /dev/null +++ b/e2e/react-start/root-document-hydration/package.json @@ -0,0 +1,40 @@ +{ + "name": "tanstack-react-start-e2e-root-document-hydration", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev --port $PORT", + "build": "vite build && tsc --noEmit", + "start": "pnpx srvx --prod -s ../client dist/server/server.js", + "test:e2e:dev": "MODE=dev playwright test --project=chromium", + "test:e2e:prod": "MODE=prod playwright test --project=chromium", + "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev && pnpm run test:e2e:prod" + }, + "dependencies": { + "@tanstack/react-router": "workspace:*", + "@tanstack/react-start": "workspace:*", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.61.0", + "@tanstack/router-e2e-utils": "workspace:*", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^6.0.1", + "srvx": "^0.11.9", + "@typescript/native": "npm:typescript@^7.0.2", + "typescript": "npm:@typescript/typescript6@^6.0.2", + "vite": "^8.0.14" + }, + "nx": { + "targets": { + "test:e2e": { + "parallelism": false + } + } + } +} diff --git a/e2e/react-start/root-document-hydration/playwright.config.ts b/e2e/react-start/root-document-hydration/playwright.config.ts new file mode 100644 index 0000000000..18abea0ab1 --- /dev/null +++ b/e2e/react-start/root-document-hydration/playwright.config.ts @@ -0,0 +1,36 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const isDev = process.env.MODE === 'dev' +const PORT = await getTestServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` + +export default defineConfig({ + testDir: './tests', + workers: 1, + reporter: [['line']], + + use: { + baseURL, + }, + + webServer: { + command: isDev ? 'pnpm dev:e2e' : `pnpm build && PORT=${PORT} pnpm start`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + env: { + PORT: String(PORT), + }, + }, + + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}) diff --git a/e2e/react-start/root-document-hydration/src/routeTree.gen.ts b/e2e/react-start/root-document-hydration/src/routeTree.gen.ts new file mode 100644 index 0000000000..dceedffdc1 --- /dev/null +++ b/e2e/react-start/root-document-hydration/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* 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 IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/react-start/root-document-hydration/src/router.tsx b/e2e/react-start/root-document-hydration/src/router.tsx new file mode 100644 index 0000000000..58f4d54e88 --- /dev/null +++ b/e2e/react-start/root-document-hydration/src/router.tsx @@ -0,0 +1,6 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ routeTree }) +} diff --git a/e2e/react-start/root-document-hydration/src/routes/__root.tsx b/e2e/react-start/root-document-hydration/src/routes/__root.tsx new file mode 100644 index 0000000000..2dc235e951 --- /dev/null +++ b/e2e/react-start/root-document-hydration/src/routes/__root.tsx @@ -0,0 +1,36 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' +import { useEffect } from 'react' + +export const Route = createRootRoute({ + pendingComponent: () => null, + component: RootComponent, +}) + +function RootComponent() { + useEffect(() => { + document.documentElement.dataset.hydrated = 'true' + }, []) + + return ( + + + + + +
+