diff --git a/docs/start/config.json b/docs/start/config.json
index 7d00579c24a..9bf039c5d13 100644
--- a/docs/start/config.json
+++ b/docs/start/config.json
@@ -113,6 +113,10 @@
"label": "Selective SSR",
"to": "framework/react/guide/selective-ssr"
},
+ {
+ "label": "Selective Hydration",
+ "to": "framework/react/guide/selective-hydration"
+ },
{
"label": "SPA Mode",
"to": "framework/react/guide/spa-mode"
diff --git a/docs/start/framework/react/guide/selective-hydration.md b/docs/start/framework/react/guide/selective-hydration.md
new file mode 100644
index 00000000000..c3a18ff93e5
--- /dev/null
+++ b/docs/start/framework/react/guide/selective-hydration.md
@@ -0,0 +1,475 @@
+---
+id: selective-hydration
+title: Selective Client-Side Hydration
+---
+
+## What is Selective Hydration?
+
+In TanStack Start, routes are server-side rendered by default and then "hydrated" on the client - meaning React attaches event handlers and makes the page interactive. The `hydrate` option gives you **page-level** control over which routes should include the React hydration bundle and become interactive on the client.
+
+**Note:** This is **page-level** selective hydration, meaning the entire page either hydrates or doesn't. For **component-level** selective hydration (Server Components), where individual components can opt in or out of hydration, stay tuned for upcoming releases from TanStack Router.
+
+When you set `hydrate: false` on a route:
+
+- ✅ The page is still server-side rendered (SSR) and SEO-friendly
+- ✅ All content loads instantly with no JavaScript required
+- ✅ External scripts from the `head()` option still work
+- ❌ React is not loaded or hydrated (no interactivity)
+- ❌ No `useState`, `useEffect`, or event handlers
+- ❌ Navigation becomes traditional full-page reloads
+
+**Important:** `hydrate: false` should only be used when you want a truly static site with absolutely no React on the client. Most applications should keep the default `hydrate: true` behavior, even for primarily static content, as you typically need at least some client-side interactivity for navigation, analytics, or other features.
+
+## How does this compare to `ssr: false`?
+
+The `ssr` and `hydrate` options serve different purposes:
+
+| Option | Controls | Use Case |
+|--------|----------|----------|
+| **`ssr`** | Server-side rendering and data loading | Control when `beforeLoad`/`loader` run and when components render on the server |
+| **`hydrate`** | Client-side React hydration | Control whether the page becomes interactive after being server-rendered |
+
+**Common Patterns:**
+
+```tsx
+// Full SSR + Hydration (default)
+ssr: true, hydrate: true
+// ✅ Renders on server ✅ Data loads on server ✅ Interactive on client
+
+// Static server-rendered page (no JavaScript)
+ssr: true, hydrate: false
+// ✅ Renders on server ✅ Data loads on server ❌ NOT interactive
+
+// Client-only page
+ssr: false, hydrate: true
+// ❌ Renders on client ❌ Data loads on client ✅ Interactive on client
+
+// This combination doesn't make sense
+ssr: false, hydrate: false
+// ❌ Nothing renders or works (avoid this)
+```
+
+**When to use `hydrate: false`:**
+- Truly static sites where you want zero React on the client
+- Pages where you're willing to give up client-side navigation and all interactivity
+- Print-only views or embedded content
+- **Note:** This is a very rare use case - most sites should use `hydrate: true` (default)
+
+**When to use `ssr: false`:**
+- Pages using browser-only APIs (localStorage, canvas)
+- Client-only routes (user dashboards, admin panels)
+- Pages with heavy client-side state
+
+## Configuration
+
+You can control whether a route includes the React hydration bundle using the `hydrate` property. This is an **opt-in/opt-out mechanism**:
+
+- **Not set (undefined)**: The default behavior is to hydrate
+- **`hydrate: true`**: Explicitly ensures hydration (useful to override a parent's `hydrate: false`)
+- **`hydrate: false`**: Explicitly disables hydration
+
+You can change the default behavior using the `defaultHydrate` option in `createStart`:
+
+```tsx
+// src/start.ts
+import { createStart } from '@tanstack/react-start'
+
+export const startInstance = createStart(() => ({
+ // Disable hydration by default
+ defaultHydrate: false,
+}))
+```
+
+### Omitting `hydrate` (default behavior)
+
+When you don't specify the `hydrate` option, the default behavior is to hydrate. The page is server-rendered and React hydrates it on the client, making it fully interactive:
+
+```tsx
+// src/routes/posts/$postId.tsx
+export const Route = createFileRoute('/posts/$postId')({
+ // hydrate not specified - will use default behavior (hydrate)
+ loader: async ({ params }) => {
+ return { post: await fetchPost(params.postId) }
+ },
+ component: PostPage,
+})
+
+function PostPage() {
+ const { post } = Route.useLoaderData()
+ const [likes, setLikes] = useState(0)
+
+ return (
+
+
{post.title}
+
{post.content}
+
setLikes(likes + 1)}>
+ Like ({likes})
+
+
+ )
+}
+```
+
+**Result:**
+- ✅ Server renders the HTML
+- ✅ Loader data is sent to the client
+- ✅ React hydrates and attaches event handlers
+- ✅ The "Like" button works
+
+### Explicitly setting `hydrate: true`
+
+You can explicitly set `hydrate: true` to **ensure** a route is always hydrated, even if a parent or nested route has `hydrate: false`. This is useful for resolving conflicts in the route tree:
+
+```tsx
+// Parent route disables hydration
+export const Route = createFileRoute('/blog')({
+ hydrate: false,
+ component: BlogLayout,
+})
+
+// Child route explicitly ensures hydration
+export const Route = createFileRoute('/blog/interactive')({
+ hydrate: true, // Explicitly opt-in to ensure hydration
+ component: InteractiveBlogPost,
+})
+```
+
+**When this creates a conflict:**
+- If a route has `hydrate: false` and a child has explicit `hydrate: true`, this creates a conflict
+- TanStack Router will **not hydrate** the page (safer default) and log a warning
+- You should resolve the conflict by making the hydration settings consistent
+
+**When to explicitly use `hydrate: true`:**
+- To document intent that a route must be hydrated
+- To override a parent's `hydrate: false` (though this creates a conflict that needs resolution)
+- To ensure hydration when `defaultHydrate: false` is set globally
+
+### `hydrate: false`
+
+This disables client-side hydration. The page is server-rendered but React is not loaded:
+
+```tsx
+// src/routes/legal/privacy.tsx
+export const Route = createFileRoute('/legal/privacy')({
+ hydrate: false,
+ loader: async () => {
+ return { lastUpdated: '2024-01-15' }
+ },
+ head: () => ({
+ meta: [
+ { title: 'Privacy Policy' },
+ { name: 'description', content: 'Our privacy policy' },
+ ],
+ // External scripts still work
+ scripts: [
+ { src: 'https://analytics.example.com/script.js' },
+ ],
+ }),
+ component: PrivacyPage,
+})
+
+function PrivacyPage() {
+ const { lastUpdated } = Route.useLoaderData()
+
+ return (
+
+
Privacy Policy
+
Last updated: {lastUpdated}
+
This is a static page with no JavaScript...
+ {/* This button won't work (no event handlers attached) */}
+
alert('This will not work')}>
+ Click me (inactive)
+
+
+ )
+}
+```
+
+**Result:**
+- ✅ Server renders the HTML with all content
+- ✅ Loader data is used during SSR
+- ✅ Meta tags and external scripts are included
+- ❌ React is NOT loaded on the client
+- ❌ No JavaScript bundle downloaded
+- ❌ Event handlers don't work
+- ❌ `useState`, `useEffect`, etc. don't run
+
+**What gets excluded when `hydrate: false`:**
+- React runtime bundle
+- React DOM bundle
+- TanStack Router client bundle
+- Your application code
+- Hydration data script (`window.$_TSR`)
+- Modulepreload links for JavaScript
+
+**What still works:**
+- Server-side rendering
+- Loader data (during SSR only)
+- Meta tags from `head()`
+- External scripts from `head()`
+- CSS and stylesheets
+- Images and static assets
+
+## Inheritance
+
+A child route inherits the `hydrate` configuration of its parent. If **any route** in the match has `hydrate: false`, the entire page will not be hydrated:
+
+```tsx
+root { hydrate: true }
+ blog { hydrate: false }
+ $postId { hydrate: true }
+```
+
+**Result:**
+- The `blog` route sets `hydrate: false`
+- Even though `$postId` sets `hydrate: true`, it inherits `false` from its parent
+- The entire page will NOT be hydrated
+
+This differs from the `ssr` option, which allows child routes to be "more restrictive" than their parents. With `hydrate`, if any route in the tree has `hydrate: false`, the entire match is treated as non-hydrated.
+
+**Why this design?**
+
+Hydration is an all-or-nothing operation for the entire page. You can't hydrate part of a React tree without hydrating its ancestors. This ensures:
+
+- ✅ Predictable behavior
+- ✅ No partial hydration issues
+- ✅ Clear mental model
+
+## Combining with `ssr` Options
+
+You can combine `ssr` and `hydrate` options for different behaviors:
+
+### Static Content Page (Server-Rendered, No JavaScript)
+
+Perfect for SEO-focused content that doesn't need interactivity:
+
+```tsx
+export const Route = createFileRoute('/blog/$slug')({
+ ssr: true, // Render on server
+ hydrate: false, // Don't load React on client
+ loader: async ({ params }) => {
+ return { post: await fetchPost(params.slug) }
+ },
+ head: ({ loaderData }) => ({
+ meta: [
+ { title: loaderData.post.title },
+ { name: 'description', content: loaderData.post.excerpt },
+ ],
+ }),
+ component: BlogPost,
+})
+```
+
+**Benefits:**
+- ⚡ Fastest possible page load (no JavaScript)
+- 🔍 Perfect SEO (fully rendered HTML)
+- 📦 Smallest possible bundle size
+
+### Client-Only Interactive Page
+
+For pages that need browser APIs:
+
+```tsx
+export const Route = createFileRoute('/dashboard')({
+ ssr: false, // Don't render on server (needs browser APIs)
+ hydrate: true, // Load React and make interactive
+ loader: () => {
+ // Runs only on client
+ return { user: getUserFromLocalStorage() }
+ },
+ component: Dashboard,
+})
+```
+
+### Hybrid: Server Data, Client Rendering
+
+Load data on server but render on client (useful for heavy visualizations):
+
+```tsx
+export const Route = createFileRoute('/reports/$id')({
+ ssr: 'data-only', // Load data on server, but don't render
+ hydrate: true, // Hydrate and render on client
+ loader: async ({ params }) => {
+ // Runs on server during SSR
+ return { report: await fetchReport(params.id) }
+ },
+ component: ReportVisualization, // Renders only on client
+})
+```
+
+## Conflict Detection
+
+The `hydrate` option is an **opt-in/opt-out mechanism**. Conflicts occur when:
+- Some routes explicitly set `hydrate: false` (opt-out)
+- Other routes explicitly set `hydrate: true` (opt-in to ensure hydration)
+
+**Note:** Routes that don't specify `hydrate` (using the default behavior) do not create conflicts.
+
+When TanStack Start detects conflicting explicit settings:
+
+1. **Does not hydrate the page** (safer default - respects the `false` setting)
+2. **Logs a warning** to help you debug:
+
+```
+⚠️ [TanStack Router] Conflicting hydrate options detected in route matches.
+Some routes have hydrate: false while others have hydrate: true.
+The page will NOT be hydrated, but this may not be the intended behavior.
+Please ensure all routes in the match have consistent hydrate settings.
+```
+
+**How to resolve conflicts:**
+- **Option 1:** Remove explicit `hydrate: true` from child routes (let them use default behavior or inherit from parent)
+- **Option 2:** Remove `hydrate: false` from parent routes if child routes need hydration
+- **Option 3:** Restructure your routes so interactive and static pages are in separate branches
+
+## Use Cases
+
+### 📄 When to use `hydrate: false`:
+
+**Important:** This is a very rare use case. Most applications should keep the default hydration behavior.
+
+Use `hydrate: false` only when:
+- You want a **truly static site** with zero React on the client
+- You're willing to give up **all client-side navigation** and interactivity
+- You want to avoid the overhead of loading React entirely
+- Examples: Print-only views, embedded content, purely informational pages
+
+### ⚡ When to explicitly use `hydrate: true`:
+
+You typically don't need to explicitly set `hydrate: true` since it's the default behavior. However, explicitly setting it is useful when:
+- **Documenting intent**: Making it clear that a route requires hydration
+- **Overriding `defaultHydrate: false`**: When you've set a global default of `false` but need specific routes to hydrate
+- **Attempting to override a parent**: Though this creates a conflict (see Conflict Detection above), you might use `hydrate: true` to signal that a child route needs hydration even if a parent has `hydrate: false`
+
+For general interactive features (forms, dashboards, real-time updates, user interactions), simply omit the `hydrate` option and use the default behavior.
+
+## Performance Impact
+
+When you use `hydrate: false`:
+
+**Bundle Size Savings:**
+- React Runtime: ~130KB (gzipped: ~45KB)
+- React DOM: ~130KB (gzipped: ~45KB)
+- TanStack Router Client: ~40KB (gzipped: ~12KB)
+- Your App Code: Varies
+
+**Total Savings:** ~300KB+ (gzipped: ~100KB+) per page
+
+**Load Time Improvements:**
+- No JavaScript parsing/execution
+- No hydration time
+- Instant interactivity (no loading state)
+
+## Example: Mixed Application
+
+A typical application might use both options:
+
+```tsx
+// Root route - enable hydration by default
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+// Marketing pages - no hydration needed
+export const Route = createFileRoute('/about')({
+ hydrate: false,
+ component: AboutPage,
+})
+
+export const Route = createFileRoute('/blog/$slug')({
+ hydrate: false,
+ loader: fetchBlogPost,
+ component: BlogPost,
+})
+
+// Legal pages - no hydration needed
+export const Route = createFileRoute('/legal/privacy')({
+ hydrate: false,
+ component: PrivacyPolicy,
+})
+
+// App pages - need hydration for interactivity
+export const Route = createFileRoute('/dashboard')({
+ hydrate: true, // explicit for clarity
+ loader: fetchDashboardData,
+ component: Dashboard,
+})
+
+export const Route = createFileRoute('/settings')({
+ hydrate: true,
+ component: SettingsPage,
+})
+```
+
+## Development Mode
+
+In development mode, React Refresh (HMR) is kept even when `hydrate: false` is set. This allows you to:
+
+- ✅ See changes instantly during development
+- ✅ Test the no-JavaScript experience in production builds
+
+To test the true `hydrate: false` experience:
+
+```bash
+# Build for production
+pnpm build
+
+# Preview the production build
+pnpm preview
+```
+
+## Troubleshooting
+
+### My page has `hydrate: false` but JavaScript is still loading
+
+**Check:**
+1. Are any parent routes setting `hydrate: true`?
+2. Are you in development mode? (React Refresh is kept for HMR)
+3. Did you rebuild after changing the option?
+
+```bash
+pnpm build
+```
+
+### My interactive features stopped working
+
+If you set `hydrate: false`, all React features will stop working:
+- Event handlers (`onClick`, `onChange`)
+- Hooks (`useState`, `useEffect`, `useQuery`)
+- Context providers
+- Client-side routing
+
+**Solution:** Explicitly set `hydrate: true` or remove the option (which defaults to hydrating).
+
+### I'm seeing hydration errors
+
+Hydration errors occur when server-rendered HTML doesn't match the client. If you have these errors:
+
+1. Consider `ssr: 'data-only'` (skip server rendering, only load data)
+2. Or use `hydrate: false` if the page doesn't need interactivity
+
+See the [Hydration Errors guide](./hydration-errors) for more details.
+
+## Summary
+
+The `hydrate` option gives you precise **page-level** control over client-side React hydration:
+
+- **Default (omitted)**: Pages hydrate by default - Full SSR + Hydration = Interactive pages
+- **`hydrate: true`**: Explicitly ensures a page is hydrated (useful for conflict resolution or documenting intent)
+- **`hydrate: false`**: Static server-rendered pages with no JavaScript
+- **Opt-in/opt-out mechanism**: Conflicts occur only when explicit `true` and `false` values are both present
+- **Inheritance**: If any route has `hydrate: false`, the page won't hydrate
+
+**Note:** This is **page-level** selective hydration. For **component-level** selective hydration (Server Components), stay tuned for upcoming releases from TanStack Router.
+
+Use `hydrate: false` for truly static pages to:
+- ⚡ Reduce bundle size
+- 🚀 Improve load times
+- 📉 Minimize JavaScript overhead
+- 🔍 Maintain perfect SEO
+
+For interactive pages, simply omit the `hydrate` option to use the default behavior:
+- 🎯 User interactions
+- 💾 Client-side state
+- 🔄 Real-time updates
+- ⚡ Dynamic behavior
diff --git a/e2e/react-start/basic-hydrate-false/.gitignore b/e2e/react-start/basic-hydrate-false/.gitignore
new file mode 100644
index 00000000000..a79d5cf1299
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/.gitignore
@@ -0,0 +1,20 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+
+/build/
+/api/
+/server/build
+/public/build
+# Sentry Config File
+.env.sentry-build-plugin
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-start/basic-hydrate-false/.prettierignore b/e2e/react-start/basic-hydrate-false/.prettierignore
new file mode 100644
index 00000000000..2be5eaa6ece
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/.prettierignore
@@ -0,0 +1,4 @@
+**/build
+**/public
+pnpm-lock.yaml
+routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/react-start/basic-hydrate-false/README.md b/e2e/react-start/basic-hydrate-false/README.md
new file mode 100644
index 00000000000..445a8066297
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/README.md
@@ -0,0 +1,152 @@
+# TanStack Router - Hydrate Feature Demo
+
+This demo showcases the new `hydrate` option for TanStack Start routes, which allows you to create server-rendered pages without client-side React hydration.
+
+## What is `hydrate: false`?
+
+The `hydrate` option controls whether a route should include the React hydration bundle and become interactive on the client. When set to `false`:
+
+- ✅ Page is fully server-side rendered
+- ✅ All content is SEO-friendly
+- ✅ Fast initial page load (no JavaScript bundle)
+- ❌ No React interactivity (no state, effects, or event handlers)
+- ❌ Navigation uses traditional full-page reloads
+
+## Running the Demo
+
+### 1. Install Dependencies
+
+```bash
+pnpm install
+```
+
+### 2. Build and Start the Server
+
+```bash
+pnpm build
+pnpm start
+```
+
+### 3. Open in Browser
+
+Navigate to `http://localhost:3000`
+
+## Demo Routes
+
+### Home Page (`/`)
+- Overview of the feature
+- Links to comparison pages
+
+### Hydrated Route (`/hydrated`)
+- **Normal React route** with full interactivity
+- Includes React bundle and hydration
+- Features:
+ - Interactive counter with state
+ - Effect hooks that run on mount
+ - Full React functionality
+
+### Static Route (`/static`)
+- **Server-rendered static page** with `hydrate: false`
+- No React bundle loaded
+- Features:
+ - Server-side rendered content
+ - Loader data still works
+ - No client-side interactivity
+ - Demonstrates what doesn't work without hydration
+
+## Code Example
+
+### Hydrated Route (Default)
+
+```typescript
+export const Route = createFileRoute('/hydrated')({
+ // hydrate: true is the default (can be omitted)
+ loader: () => ({ message: 'Server data' }),
+ component: MyComponent,
+})
+```
+
+### Static Route
+
+```typescript
+export const Route = createFileRoute('/static')({
+ hydrate: false, // 🔑 This is the key option
+ loader: () => ({ message: 'Server data' }),
+ component: MyStaticComponent,
+})
+```
+
+## Inspecting the Difference
+
+### Open DevTools → Network Tab
+
+1. Visit `/hydrated` and filter by "JS"
+ - You'll see: React, React DOM, Router bundles, and app code
+ - Look for hydration data in the HTML (`window.$_TSR`)
+
+2. Visit `/static` and filter by "JS"
+ - You'll see: NO application bundles
+ - NO hydration data in the HTML
+ - Only external scripts you explicitly added
+
+### Check Page Source
+
+**Hydrated Route:**
+```html
+
+
+```
+
+**Static Route:**
+```html
+
+```
+
+## Use Cases
+
+Perfect for:
+- 📄 Legal pages (Terms, Privacy Policy)
+- 📝 Blog posts and articles
+- 🎯 Marketing landing pages
+- 📚 Documentation
+- 🔍 SEO-focused content pages
+- ⚡ When you want minimal JavaScript
+
+## Advanced Features
+
+### Inheritance
+If a parent route has `hydrate: false`, all child routes inherit it unless explicitly overridden.
+
+### Dynamic Hydration
+You can use a function to determine hydration dynamically:
+
+```typescript
+export const Route = createFileRoute('/dynamic')({
+ hydrate: ({ search, params }) => {
+ // Conditionally hydrate based on query params or other factors
+ return search.interactive === 'true'
+ },
+ component: MyComponent,
+})
+```
+
+### Conflict Warning
+If conflicting `hydrate` settings exist in the route tree (some true, some false), the page will hydrate and log a warning to help you debug.
+
+## Running Tests
+
+```bash
+pnpm test:e2e
+```
+
+The E2E tests verify:
+- Main bundle scripts are excluded when `hydrate: false`
+- Serialized data is excluded
+- External scripts still work
+- Server-rendered content is correct
+- Meta tags are properly rendered
+
+## Learn More
+
+- [TanStack Router Documentation](https://tanstack.com/router)
+- [TanStack Start Documentation](https://tanstack.com/router/latest/docs/framework/react/start)
diff --git a/e2e/react-start/basic-hydrate-false/package.json b/e2e/react-start/basic-hydrate-false/package.json
new file mode 100644
index 00000000000..27cccea7a47
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "tanstack-react-start-e2e-basic-hydrate-false",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev",
+ "build": "vite build && tsc --noEmit",
+ "build:spa": "MODE=spa vite build && tsc --noEmit",
+ "build:prerender": "MODE=prerender vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+ "start:spa": "node server.js",
+ "test:e2e:startDummyServer": "node -e 'import(\"./tests/setup/global.setup.ts\").then(m => m.default())' &",
+ "test:e2e:stopDummyServer": "node -e 'import(\"./tests/setup/global.teardown.ts\").then(m => m.default())'",
+ "test:e2e:spaMode": "rm -rf port*.txt; MODE=spa playwright test --project=chromium",
+ "test:e2e:ssrMode": "rm -rf port*.txt; playwright test --project=chromium",
+ "test:e2e:prerender": "rm -rf port*.txt; MODE=prerender playwright test --project=chromium",
+ "test:e2e:preview": "rm -rf port*.txt; MODE=preview playwright test --project=chromium",
+ "test:e2e": "pnpm run test:e2e:spaMode && pnpm run test:e2e:ssrMode && pnpm run test:e2e:prerender && pnpm run test:e2e:preview"
+ },
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/react-router-devtools": "workspace:^",
+ "@tanstack/react-start": "workspace:^",
+ "express": "^5.1.0",
+ "http-proxy-middleware": "^3.0.5",
+ "js-cookie": "^3.0.5",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "redaxios": "^0.5.1",
+ "tailwind-merge": "^2.6.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/js-cookie": "^3.0.6",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.4",
+ "combinate": "^1.1.11",
+ "postcss": "^8.5.1",
+ "srvx": "^0.8.6",
+ "tailwindcss": "^4.1.17",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-tsconfig-paths": "^5.1.4",
+ "zod": "^3.24.2"
+ }
+}
diff --git a/e2e/react-start/basic-hydrate-false/playwright.config.ts b/e2e/react-start/basic-hydrate-false/playwright.config.ts
new file mode 100644
index 00000000000..aa29067f463
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/playwright.config.ts
@@ -0,0 +1,72 @@
+import { defineConfig, devices } from '@playwright/test'
+import {
+ getDummyServerPort,
+ getTestServerPort,
+} from '@tanstack/router-e2e-utils'
+import { isSpaMode } from './tests/utils/isSpaMode'
+import { isPrerender } from './tests/utils/isPrerender'
+import { isPreview } from './tests/utils/isPreview'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(
+ `${packageJson.name}${isSpaMode ? '_spa' : ''}${isPreview ? '_preview' : ''}`,
+)
+const START_PORT = await getTestServerPort(
+ `${packageJson.name}${isSpaMode ? '_spa_start' : ''}`,
+)
+const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+const spaModeCommand = `pnpm build:spa && pnpm start:spa`
+const ssrModeCommand = `pnpm build && pnpm start`
+const prerenderModeCommand = `pnpm run test:e2e:startDummyServer && pnpm build:prerender && pnpm run test:e2e:stopDummyServer && pnpm start`
+const previewModeCommand = `pnpm build && pnpm preview --port ${PORT}`
+
+const getCommand = () => {
+ if (isSpaMode) return spaModeCommand
+ if (isPrerender) return prerenderModeCommand
+ if (isPreview) return previewModeCommand
+ return ssrModeCommand
+}
+console.log('running in spa mode: ', isSpaMode.toString())
+console.log('running in prerender mode: ', isPrerender.toString())
+console.log('running in preview mode: ', isPreview.toString())
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+ reporter: [['line']],
+
+ globalSetup: './tests/setup/global.setup.ts',
+ globalTeardown: './tests/setup/global.teardown.ts',
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: getCommand(),
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ env: {
+ MODE: process.env.MODE || '',
+ VITE_NODE_ENV: 'test',
+ VITE_EXTERNAL_PORT: String(EXTERNAL_PORT),
+ VITE_SERVER_PORT: String(PORT),
+ START_PORT: String(START_PORT),
+ PORT: String(PORT),
+ },
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+})
diff --git a/e2e/react-start/basic-hydrate-false/postcss.config.mjs b/e2e/react-start/basic-hydrate-false/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/react-start/basic-hydrate-false/public/android-chrome-192x192.png b/e2e/react-start/basic-hydrate-false/public/android-chrome-192x192.png
new file mode 100644
index 00000000000..09c8324f8c6
Binary files /dev/null and b/e2e/react-start/basic-hydrate-false/public/android-chrome-192x192.png differ
diff --git a/e2e/react-start/basic-hydrate-false/public/android-chrome-512x512.png b/e2e/react-start/basic-hydrate-false/public/android-chrome-512x512.png
new file mode 100644
index 00000000000..11d626ea3d0
Binary files /dev/null and b/e2e/react-start/basic-hydrate-false/public/android-chrome-512x512.png differ
diff --git a/e2e/react-start/basic-hydrate-false/public/apple-touch-icon.png b/e2e/react-start/basic-hydrate-false/public/apple-touch-icon.png
new file mode 100644
index 00000000000..5a9423cc02c
Binary files /dev/null and b/e2e/react-start/basic-hydrate-false/public/apple-touch-icon.png differ
diff --git a/e2e/react-start/basic-hydrate-false/public/favicon-16x16.png b/e2e/react-start/basic-hydrate-false/public/favicon-16x16.png
new file mode 100644
index 00000000000..e3389b00443
Binary files /dev/null and b/e2e/react-start/basic-hydrate-false/public/favicon-16x16.png differ
diff --git a/e2e/react-start/basic-hydrate-false/public/favicon-32x32.png b/e2e/react-start/basic-hydrate-false/public/favicon-32x32.png
new file mode 100644
index 00000000000..900c77d444c
Binary files /dev/null and b/e2e/react-start/basic-hydrate-false/public/favicon-32x32.png differ
diff --git a/e2e/react-start/basic-hydrate-false/public/favicon.ico b/e2e/react-start/basic-hydrate-false/public/favicon.ico
new file mode 100644
index 00000000000..1a1751676f7
Binary files /dev/null and b/e2e/react-start/basic-hydrate-false/public/favicon.ico differ
diff --git a/e2e/react-start/basic-hydrate-false/public/favicon.png b/e2e/react-start/basic-hydrate-false/public/favicon.png
new file mode 100644
index 00000000000..1e77bc06091
Binary files /dev/null and b/e2e/react-start/basic-hydrate-false/public/favicon.png differ
diff --git a/e2e/react-start/basic-hydrate-false/public/script.js b/e2e/react-start/basic-hydrate-false/public/script.js
new file mode 100644
index 00000000000..897477e7d0a
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/public/script.js
@@ -0,0 +1,2 @@
+console.log('SCRIPT_1 loaded')
+window.SCRIPT_1 = true
diff --git a/e2e/react-start/basic-hydrate-false/public/script2.js b/e2e/react-start/basic-hydrate-false/public/script2.js
new file mode 100644
index 00000000000..819af30daf9
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/public/script2.js
@@ -0,0 +1,2 @@
+console.log('SCRIPT_2 loaded')
+window.SCRIPT_2 = true
diff --git a/e2e/react-start/basic-hydrate-false/public/site.webmanifest b/e2e/react-start/basic-hydrate-false/public/site.webmanifest
new file mode 100644
index 00000000000..fa99de77db6
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/public/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/e2e/react-start/basic-hydrate-false/server.js b/e2e/react-start/basic-hydrate-false/server.js
new file mode 100644
index 00000000000..d618ab4bce3
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/server.js
@@ -0,0 +1,67 @@
+import { toNodeHandler } from 'srvx/node'
+import path from 'node:path'
+import express from 'express'
+import { createProxyMiddleware } from 'http-proxy-middleware'
+
+const port = process.env.PORT || 3000
+
+const startPort = process.env.START_PORT || 3001
+
+export async function createStartServer() {
+ const server = (await import('./dist/server/server.js')).default
+ const nodeHandler = toNodeHandler(server.fetch)
+
+ const app = express()
+
+ app.use(express.static('./dist/client'))
+
+ app.use(async (req, res, next) => {
+ try {
+ await nodeHandler(req, res)
+ } catch (error) {
+ next(error)
+ }
+ })
+
+ return { app }
+}
+
+export async function createSpaServer() {
+ const app = express()
+
+ app.use(
+ '/api',
+ createProxyMiddleware({
+ target: `http://localhost:${startPort}/api`, // Replace with your target server's URL
+ changeOrigin: false, // Needed for virtual hosted sites,
+ }),
+ )
+
+ app.use(
+ '/_serverFn',
+ createProxyMiddleware({
+ target: `http://localhost:${startPort}/_serverFn`, // Replace with your target server's URL
+ changeOrigin: false, // Needed for virtual hosted sites,
+ }),
+ )
+
+ app.use(express.static('./dist/client'))
+
+ app.get('/{*splat}', (req, res) => {
+ res.sendFile(path.resolve('./dist/client/index.html'))
+ })
+
+ return { app }
+}
+
+createSpaServer().then(async ({ app }) =>
+ app.listen(port, () => {
+ console.info(`Client Server: http://localhost:${port}`)
+ }),
+)
+
+createStartServer().then(async ({ app }) =>
+ app.listen(startPort, () => {
+ console.info(`Start Server: http://localhost:${startPort}`)
+ }),
+)
diff --git a/e2e/react-start/basic-hydrate-false/src/client.tsx b/e2e/react-start/basic-hydrate-false/src/client.tsx
new file mode 100644
index 00000000000..fdfbde86770
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/client.tsx
@@ -0,0 +1,16 @@
+// DO NOT DELETE THIS FILE!!!
+// This file is a good smoke test to make sure the custom client entry is working
+import { StrictMode, startTransition } from 'react'
+import { hydrateRoot } from 'react-dom/client'
+import { StartClient } from '@tanstack/react-start/client'
+
+console.log("[client-entry]: using custom client entry in 'src/client.tsx'")
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+ ,
+ )
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/components/CustomMessage.tsx b/e2e/react-start/basic-hydrate-false/src/components/CustomMessage.tsx
new file mode 100644
index 00000000000..d00e4eac60b
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/components/CustomMessage.tsx
@@ -0,0 +1,10 @@
+import * as React from 'react'
+
+export function CustomMessage({ message }: { message: string }) {
+ return (
+
+
This is a custom message:
+
{message}
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/components/DefaultCatchBoundary.tsx b/e2e/react-start/basic-hydrate-false/src/components/DefaultCatchBoundary.tsx
new file mode 100644
index 00000000000..ef2daa1ea1d
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/react-router'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+ {
+ router.invalidate()
+ }}
+ className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
+ >
+ Try Again
+
+ {isRoot ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/components/NotFound.tsx b/e2e/react-start/basic-hydrate-false/src/components/NotFound.tsx
new file mode 100644
index 00000000000..4e84e3f8e00
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/react-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+ window.history.back()}
+ className="bg-emerald-500 text-white px-2 py-1 rounded-sm uppercase font-black text-sm"
+ >
+ Go back
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/components/RedirectOnClick.tsx b/e2e/react-start/basic-hydrate-false/src/components/RedirectOnClick.tsx
new file mode 100644
index 00000000000..37b434c2e7d
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/components/RedirectOnClick.tsx
@@ -0,0 +1,26 @@
+import { useServerFn } from '@tanstack/react-start'
+import { throwRedirect } from './throwRedirect'
+
+interface RedirectOnClickProps {
+ target: 'internal' | 'external'
+ reloadDocument?: boolean
+ externalHost?: string
+}
+
+export function RedirectOnClick({
+ target,
+ reloadDocument,
+ externalHost,
+}: RedirectOnClickProps) {
+ const execute = useServerFn(throwRedirect)
+ return (
+
+ execute({ data: { target, reloadDocument, externalHost } })
+ }
+ >
+ click me
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/components/throwRedirect.ts b/e2e/react-start/basic-hydrate-false/src/components/throwRedirect.ts
new file mode 100644
index 00000000000..0081a3c5602
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/components/throwRedirect.ts
@@ -0,0 +1,23 @@
+import { redirect } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+
+export const throwRedirect = createServerFn()
+ .inputValidator(
+ (opts: {
+ target: 'internal' | 'external'
+ reloadDocument?: boolean
+ externalHost?: string
+ }) => opts,
+ )
+ .handler((ctx) => {
+ if (ctx.data.target === 'internal') {
+ throw redirect({
+ to: '/posts',
+ reloadDocument: ctx.data.reloadDocument,
+ })
+ }
+ const href = ctx.data.externalHost ?? 'http://example.com'
+ throw redirect({
+ href,
+ })
+ })
diff --git a/e2e/react-start/basic-hydrate-false/src/routeTree.gen.ts b/e2e/react-start/basic-hydrate-false/src/routeTree.gen.ts
new file mode 100644
index 00000000000..98eaa38ffee
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routeTree.gen.ts
@@ -0,0 +1,1078 @@
+/* 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 { createFileRoute } from '@tanstack/react-router'
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
+import { Route as UsersRouteImport } from './routes/users'
+import { Route as StreamRouteImport } from './routes/stream'
+import { Route as StaticRouteImport } from './routes/static'
+import { Route as ScriptsRouteImport } from './routes/scripts'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as LinksRouteImport } from './routes/links'
+import { Route as InlineScriptsRouteImport } from './routes/inline-scripts'
+import { Route as HydratedRouteImport } from './routes/hydrated'
+import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
+import { Route as NotFoundRouteRouteImport } from './routes/not-found/route'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as SearchParamsIndexRouteImport } from './routes/search-params/index'
+import { Route as RedirectIndexRouteImport } from './routes/redirect/index'
+import { Route as PostsIndexRouteImport } from './routes/posts.index'
+import { Route as NotFoundIndexRouteImport } from './routes/not-found/index'
+import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-cookie-redirect/index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
+import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
+import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
+import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
+import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
+import { Route as NotFoundViaHeadRouteImport } from './routes/not-found/via-head'
+import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
+import { Route as MultiCookieRedirectTargetRouteImport } from './routes/multi-cookie-redirect/target'
+import { Route as ApiUsersRouteImport } from './routes/api.users'
+import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as RedirectTargetIndexRouteImport } from './routes/redirect/$target/index'
+import { Route as RedirectTargetViaLoaderRouteImport } from './routes/redirect/$target/via-loader'
+import { Route as RedirectTargetViaBeforeLoadRouteImport } from './routes/redirect/$target/via-beforeLoad'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
+import { Route as ApiUsersIdRouteImport } from './routes/api/users.$id'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as RedirectTargetServerFnIndexRouteImport } from './routes/redirect/$target/serverFn/index'
+import { Route as RedirectTargetServerFnViaUseServerFnRouteImport } from './routes/redirect/$target/serverFn/via-useServerFn'
+import { Route as RedirectTargetServerFnViaLoaderRouteImport } from './routes/redirect/$target/serverFn/via-loader'
+import { Route as RedirectTargetServerFnViaBeforeLoadRouteImport } from './routes/redirect/$target/serverFn/via-beforeLoad'
+import { Route as FooBarQuxHereRouteImport } from './routes/foo/$bar/$qux/_here'
+import { Route as FooBarQuxHereIndexRouteImport } from './routes/foo/$bar/$qux/_here/index'
+
+const FooBarQuxRouteImport = createFileRoute('/foo/$bar/$qux')()
+
+const Char45824Char54620Char48124Char44397Route =
+ Char45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const UsersRoute = UsersRouteImport.update({
+ id: '/users',
+ path: '/users',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const StreamRoute = StreamRouteImport.update({
+ id: '/stream',
+ path: '/stream',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const StaticRoute = StaticRouteImport.update({
+ id: '/static',
+ path: '/static',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ScriptsRoute = ScriptsRouteImport.update({
+ id: '/scripts',
+ path: '/scripts',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LinksRoute = LinksRouteImport.update({
+ id: '/links',
+ path: '/links',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const InlineScriptsRoute = InlineScriptsRouteImport.update({
+ id: '/inline-scripts',
+ path: '/inline-scripts',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const HydratedRoute = HydratedRouteImport.update({
+ id: '/hydrated',
+ path: '/hydrated',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const DeferredRoute = DeferredRouteImport.update({
+ id: '/deferred',
+ path: '/deferred',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LayoutRoute = LayoutRouteImport.update({
+ id: '/_layout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const SearchParamsRouteRoute = SearchParamsRouteRouteImport.update({
+ id: '/search-params',
+ path: '/search-params',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const NotFoundRouteRoute = NotFoundRouteRouteImport.update({
+ id: '/not-found',
+ path: '/not-found',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const UsersIndexRoute = UsersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => UsersRoute,
+} as any)
+const SearchParamsIndexRoute = SearchParamsIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => SearchParamsRouteRoute,
+} as any)
+const RedirectIndexRoute = RedirectIndexRouteImport.update({
+ id: '/redirect/',
+ path: '/redirect/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsIndexRoute = PostsIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
+} as any)
+const NotFoundIndexRoute = NotFoundIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
+const MultiCookieRedirectIndexRoute =
+ MultiCookieRedirectIndexRouteImport.update({
+ id: '/multi-cookie-redirect/',
+ path: '/multi-cookie-redirect/',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => UsersRoute,
+} as any)
+const SearchParamsLoaderThrowsRedirectRoute =
+ SearchParamsLoaderThrowsRedirectRouteImport.update({
+ id: '/loader-throws-redirect',
+ path: '/loader-throws-redirect',
+ getParentRoute: () => SearchParamsRouteRoute,
+ } as any)
+const SearchParamsDefaultRoute = SearchParamsDefaultRouteImport.update({
+ id: '/default',
+ path: '/default',
+ getParentRoute: () => SearchParamsRouteRoute,
+} as any)
+const RedirectTargetRoute = RedirectTargetRouteImport.update({
+ id: '/redirect/$target',
+ path: '/redirect/$target',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
+} as any)
+const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
+ id: '/via-loader',
+ path: '/via-loader',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
+const NotFoundViaHeadRoute = NotFoundViaHeadRouteImport.update({
+ id: '/via-head',
+ path: '/via-head',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
+const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadRouteImport.update({
+ id: '/via-beforeLoad',
+ path: '/via-beforeLoad',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
+const MultiCookieRedirectTargetRoute =
+ MultiCookieRedirectTargetRouteImport.update({
+ id: '/multi-cookie-redirect/target',
+ path: '/multi-cookie-redirect/target',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LayoutLayout2Route = LayoutLayout2RouteImport.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
+} as any)
+const FooBarQuxRoute = FooBarQuxRouteImport.update({
+ id: '/foo/$bar/$qux',
+ path: '/foo/$bar/$qux',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RedirectTargetIndexRoute = RedirectTargetIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => RedirectTargetRoute,
+} as any)
+const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderRouteImport.update({
+ id: '/via-loader',
+ path: '/via-loader',
+ getParentRoute: () => RedirectTargetRoute,
+} as any)
+const RedirectTargetViaBeforeLoadRoute =
+ RedirectTargetViaBeforeLoadRouteImport.update({
+ id: '/via-beforeLoad',
+ path: '/via-beforeLoad',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ApiUsersIdRoute = ApiUsersIdRouteImport.update({
+ id: '/$id',
+ path: '/$id',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+const RedirectTargetServerFnIndexRoute =
+ RedirectTargetServerFnIndexRouteImport.update({
+ id: '/serverFn/',
+ path: '/serverFn/',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+const RedirectTargetServerFnViaUseServerFnRoute =
+ RedirectTargetServerFnViaUseServerFnRouteImport.update({
+ id: '/serverFn/via-useServerFn',
+ path: '/serverFn/via-useServerFn',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+const RedirectTargetServerFnViaLoaderRoute =
+ RedirectTargetServerFnViaLoaderRouteImport.update({
+ id: '/serverFn/via-loader',
+ path: '/serverFn/via-loader',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+const RedirectTargetServerFnViaBeforeLoadRoute =
+ RedirectTargetServerFnViaBeforeLoadRouteImport.update({
+ id: '/serverFn/via-beforeLoad',
+ path: '/serverFn/via-beforeLoad',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+const FooBarQuxHereRoute = FooBarQuxHereRouteImport.update({
+ id: '/_here',
+ getParentRoute: () => FooBarQuxRoute,
+} as any)
+const FooBarQuxHereIndexRoute = FooBarQuxHereIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => FooBarQuxHereRoute,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/not-found': typeof NotFoundRouteRouteWithChildren
+ '/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/deferred': typeof DeferredRoute
+ '/hydrated': typeof HydratedRoute
+ '/inline-scripts': typeof InlineScriptsRoute
+ '/links': typeof LinksRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/scripts': typeof ScriptsRoute
+ '/static': typeof StaticRoute
+ '/stream': typeof StreamRoute
+ '/users': typeof UsersRouteWithChildren
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
+ '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
+ '/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/redirect/$target': typeof RedirectTargetRouteWithChildren
+ '/search-params/default': typeof SearchParamsDefaultRoute
+ '/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/users/$userId': typeof UsersUserIdRoute
+ '/multi-cookie-redirect': typeof MultiCookieRedirectIndexRoute
+ '/not-found/': typeof NotFoundIndexRoute
+ '/posts/': typeof PostsIndexRoute
+ '/redirect': typeof RedirectIndexRoute
+ '/search-params/': typeof SearchParamsIndexRoute
+ '/users/': typeof UsersIndexRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/api/users/$id': typeof ApiUsersIdRoute
+ '/posts/$postId/deep': typeof PostsPostIdDeepRoute
+ '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+ '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/redirect/$target/': typeof RedirectTargetIndexRoute
+ '/foo/$bar/$qux': typeof FooBarQuxHereRouteWithChildren
+ '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+ '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+ '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+ '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute
+ '/foo/$bar/$qux/': typeof FooBarQuxHereIndexRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/deferred': typeof DeferredRoute
+ '/hydrated': typeof HydratedRoute
+ '/inline-scripts': typeof InlineScriptsRoute
+ '/links': typeof LinksRoute
+ '/scripts': typeof ScriptsRoute
+ '/static': typeof StaticRoute
+ '/stream': typeof StreamRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
+ '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
+ '/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/search-params/default': typeof SearchParamsDefaultRoute
+ '/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/users/$userId': typeof UsersUserIdRoute
+ '/multi-cookie-redirect': typeof MultiCookieRedirectIndexRoute
+ '/not-found': typeof NotFoundIndexRoute
+ '/posts': typeof PostsIndexRoute
+ '/redirect': typeof RedirectIndexRoute
+ '/search-params': typeof SearchParamsIndexRoute
+ '/users': typeof UsersIndexRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/api/users/$id': typeof ApiUsersIdRoute
+ '/posts/$postId/deep': typeof PostsPostIdDeepRoute
+ '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+ '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/redirect/$target': typeof RedirectTargetIndexRoute
+ '/foo/$bar/$qux': typeof FooBarQuxHereIndexRoute
+ '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+ '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+ '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+ '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/not-found': typeof NotFoundRouteRouteWithChildren
+ '/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/_layout': typeof LayoutRouteWithChildren
+ '/deferred': typeof DeferredRoute
+ '/hydrated': typeof HydratedRoute
+ '/inline-scripts': typeof InlineScriptsRoute
+ '/links': typeof LinksRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/scripts': typeof ScriptsRoute
+ '/static': typeof StaticRoute
+ '/stream': typeof StreamRoute
+ '/users': typeof UsersRouteWithChildren
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
+ '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
+ '/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/redirect/$target': typeof RedirectTargetRouteWithChildren
+ '/search-params/default': typeof SearchParamsDefaultRoute
+ '/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/users/$userId': typeof UsersUserIdRoute
+ '/multi-cookie-redirect/': typeof MultiCookieRedirectIndexRoute
+ '/not-found/': typeof NotFoundIndexRoute
+ '/posts/': typeof PostsIndexRoute
+ '/redirect/': typeof RedirectIndexRoute
+ '/search-params/': typeof SearchParamsIndexRoute
+ '/users/': typeof UsersIndexRoute
+ '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
+ '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/api/users/$id': typeof ApiUsersIdRoute
+ '/posts_/$postId/deep': typeof PostsPostIdDeepRoute
+ '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+ '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/redirect/$target/': typeof RedirectTargetIndexRoute
+ '/foo/$bar/$qux': typeof FooBarQuxRouteWithChildren
+ '/foo/$bar/$qux/_here': typeof FooBarQuxHereRouteWithChildren
+ '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+ '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+ '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+ '/redirect/$target/serverFn/': typeof RedirectTargetServerFnIndexRoute
+ '/foo/$bar/$qux/_here/': typeof FooBarQuxHereIndexRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/not-found'
+ | '/search-params'
+ | '/deferred'
+ | '/hydrated'
+ | '/inline-scripts'
+ | '/links'
+ | '/posts'
+ | '/scripts'
+ | '/static'
+ | '/stream'
+ | '/users'
+ | '/대한민국'
+ | '/api/users'
+ | '/multi-cookie-redirect/target'
+ | '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
+ | '/not-found/via-loader'
+ | '/posts/$postId'
+ | '/redirect/$target'
+ | '/search-params/default'
+ | '/search-params/loader-throws-redirect'
+ | '/users/$userId'
+ | '/multi-cookie-redirect'
+ | '/not-found/'
+ | '/posts/'
+ | '/redirect'
+ | '/search-params/'
+ | '/users/'
+ | '/layout-a'
+ | '/layout-b'
+ | '/api/users/$id'
+ | '/posts/$postId/deep'
+ | '/redirect/$target/via-beforeLoad'
+ | '/redirect/$target/via-loader'
+ | '/redirect/$target/'
+ | '/foo/$bar/$qux'
+ | '/redirect/$target/serverFn/via-beforeLoad'
+ | '/redirect/$target/serverFn/via-loader'
+ | '/redirect/$target/serverFn/via-useServerFn'
+ | '/redirect/$target/serverFn'
+ | '/foo/$bar/$qux/'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/deferred'
+ | '/hydrated'
+ | '/inline-scripts'
+ | '/links'
+ | '/scripts'
+ | '/static'
+ | '/stream'
+ | '/대한민국'
+ | '/api/users'
+ | '/multi-cookie-redirect/target'
+ | '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
+ | '/not-found/via-loader'
+ | '/posts/$postId'
+ | '/search-params/default'
+ | '/search-params/loader-throws-redirect'
+ | '/users/$userId'
+ | '/multi-cookie-redirect'
+ | '/not-found'
+ | '/posts'
+ | '/redirect'
+ | '/search-params'
+ | '/users'
+ | '/layout-a'
+ | '/layout-b'
+ | '/api/users/$id'
+ | '/posts/$postId/deep'
+ | '/redirect/$target/via-beforeLoad'
+ | '/redirect/$target/via-loader'
+ | '/redirect/$target'
+ | '/foo/$bar/$qux'
+ | '/redirect/$target/serverFn/via-beforeLoad'
+ | '/redirect/$target/serverFn/via-loader'
+ | '/redirect/$target/serverFn/via-useServerFn'
+ | '/redirect/$target/serverFn'
+ id:
+ | '__root__'
+ | '/'
+ | '/not-found'
+ | '/search-params'
+ | '/_layout'
+ | '/deferred'
+ | '/hydrated'
+ | '/inline-scripts'
+ | '/links'
+ | '/posts'
+ | '/scripts'
+ | '/static'
+ | '/stream'
+ | '/users'
+ | '/대한민국'
+ | '/_layout/_layout-2'
+ | '/api/users'
+ | '/multi-cookie-redirect/target'
+ | '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
+ | '/not-found/via-loader'
+ | '/posts/$postId'
+ | '/redirect/$target'
+ | '/search-params/default'
+ | '/search-params/loader-throws-redirect'
+ | '/users/$userId'
+ | '/multi-cookie-redirect/'
+ | '/not-found/'
+ | '/posts/'
+ | '/redirect/'
+ | '/search-params/'
+ | '/users/'
+ | '/_layout/_layout-2/layout-a'
+ | '/_layout/_layout-2/layout-b'
+ | '/api/users/$id'
+ | '/posts_/$postId/deep'
+ | '/redirect/$target/via-beforeLoad'
+ | '/redirect/$target/via-loader'
+ | '/redirect/$target/'
+ | '/foo/$bar/$qux'
+ | '/foo/$bar/$qux/_here'
+ | '/redirect/$target/serverFn/via-beforeLoad'
+ | '/redirect/$target/serverFn/via-loader'
+ | '/redirect/$target/serverFn/via-useServerFn'
+ | '/redirect/$target/serverFn/'
+ | '/foo/$bar/$qux/_here/'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ NotFoundRouteRoute: typeof NotFoundRouteRouteWithChildren
+ SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
+ LayoutRoute: typeof LayoutRouteWithChildren
+ DeferredRoute: typeof DeferredRoute
+ HydratedRoute: typeof HydratedRoute
+ InlineScriptsRoute: typeof InlineScriptsRoute
+ LinksRoute: typeof LinksRoute
+ PostsRoute: typeof PostsRouteWithChildren
+ ScriptsRoute: typeof ScriptsRoute
+ StaticRoute: typeof StaticRoute
+ StreamRoute: typeof StreamRoute
+ UsersRoute: typeof UsersRouteWithChildren
+ Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
+ ApiUsersRoute: typeof ApiUsersRouteWithChildren
+ MultiCookieRedirectTargetRoute: typeof MultiCookieRedirectTargetRoute
+ RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
+ MultiCookieRedirectIndexRoute: typeof MultiCookieRedirectIndexRoute
+ RedirectIndexRoute: typeof RedirectIndexRoute
+ PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
+ FooBarQuxRoute: typeof FooBarQuxRouteWithChildren
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/stream': {
+ id: '/stream'
+ path: '/stream'
+ fullPath: '/stream'
+ preLoaderRoute: typeof StreamRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/static': {
+ id: '/static'
+ path: '/static'
+ fullPath: '/static'
+ preLoaderRoute: typeof StaticRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/scripts': {
+ id: '/scripts'
+ path: '/scripts'
+ fullPath: '/scripts'
+ preLoaderRoute: typeof ScriptsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/links': {
+ id: '/links'
+ path: '/links'
+ fullPath: '/links'
+ preLoaderRoute: typeof LinksRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/inline-scripts': {
+ id: '/inline-scripts'
+ path: '/inline-scripts'
+ fullPath: '/inline-scripts'
+ preLoaderRoute: typeof InlineScriptsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/hydrated': {
+ id: '/hydrated'
+ path: '/hydrated'
+ fullPath: '/hydrated'
+ preLoaderRoute: typeof HydratedRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/deferred': {
+ id: '/deferred'
+ path: '/deferred'
+ fullPath: '/deferred'
+ preLoaderRoute: typeof DeferredRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/search-params': {
+ id: '/search-params'
+ path: '/search-params'
+ fullPath: '/search-params'
+ preLoaderRoute: typeof SearchParamsRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/not-found': {
+ id: '/not-found'
+ path: '/not-found'
+ fullPath: '/not-found'
+ preLoaderRoute: typeof NotFoundRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/search-params/': {
+ id: '/search-params/'
+ path: '/'
+ fullPath: '/search-params/'
+ preLoaderRoute: typeof SearchParamsIndexRouteImport
+ parentRoute: typeof SearchParamsRouteRoute
+ }
+ '/redirect/': {
+ id: '/redirect/'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof PostsIndexRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/not-found/': {
+ id: '/not-found/'
+ path: '/'
+ fullPath: '/not-found/'
+ preLoaderRoute: typeof NotFoundIndexRouteImport
+ parentRoute: typeof NotFoundRouteRoute
+ }
+ '/multi-cookie-redirect/': {
+ id: '/multi-cookie-redirect/'
+ path: '/multi-cookie-redirect'
+ fullPath: '/multi-cookie-redirect'
+ preLoaderRoute: typeof MultiCookieRedirectIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/users/$userId': {
+ id: '/users/$userId'
+ path: '/$userId'
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/search-params/loader-throws-redirect': {
+ id: '/search-params/loader-throws-redirect'
+ path: '/loader-throws-redirect'
+ fullPath: '/search-params/loader-throws-redirect'
+ preLoaderRoute: typeof SearchParamsLoaderThrowsRedirectRouteImport
+ parentRoute: typeof SearchParamsRouteRoute
+ }
+ '/search-params/default': {
+ id: '/search-params/default'
+ path: '/default'
+ fullPath: '/search-params/default'
+ preLoaderRoute: typeof SearchParamsDefaultRouteImport
+ parentRoute: typeof SearchParamsRouteRoute
+ }
+ '/redirect/$target': {
+ id: '/redirect/$target'
+ path: '/redirect/$target'
+ fullPath: '/redirect/$target'
+ preLoaderRoute: typeof RedirectTargetRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsPostIdRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/not-found/via-loader': {
+ id: '/not-found/via-loader'
+ path: '/via-loader'
+ fullPath: '/not-found/via-loader'
+ preLoaderRoute: typeof NotFoundViaLoaderRouteImport
+ parentRoute: typeof NotFoundRouteRoute
+ }
+ '/not-found/via-head': {
+ id: '/not-found/via-head'
+ path: '/via-head'
+ fullPath: '/not-found/via-head'
+ preLoaderRoute: typeof NotFoundViaHeadRouteImport
+ parentRoute: typeof NotFoundRouteRoute
+ }
+ '/not-found/via-beforeLoad': {
+ id: '/not-found/via-beforeLoad'
+ path: '/via-beforeLoad'
+ fullPath: '/not-found/via-beforeLoad'
+ preLoaderRoute: typeof NotFoundViaBeforeLoadRouteImport
+ parentRoute: typeof NotFoundRouteRoute
+ }
+ '/multi-cookie-redirect/target': {
+ id: '/multi-cookie-redirect/target'
+ path: '/multi-cookie-redirect/target'
+ fullPath: '/multi-cookie-redirect/target'
+ preLoaderRoute: typeof MultiCookieRedirectTargetRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutLayout2RouteImport
+ parentRoute: typeof LayoutRoute
+ }
+ '/foo/$bar/$qux': {
+ id: '/foo/$bar/$qux'
+ path: '/foo/$bar/$qux'
+ fullPath: '/foo/$bar/$qux'
+ preLoaderRoute: typeof FooBarQuxRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/redirect/$target/': {
+ id: '/redirect/$target/'
+ path: '/'
+ fullPath: '/redirect/$target/'
+ preLoaderRoute: typeof RedirectTargetIndexRouteImport
+ parentRoute: typeof RedirectTargetRoute
+ }
+ '/redirect/$target/via-loader': {
+ id: '/redirect/$target/via-loader'
+ path: '/via-loader'
+ fullPath: '/redirect/$target/via-loader'
+ preLoaderRoute: typeof RedirectTargetViaLoaderRouteImport
+ parentRoute: typeof RedirectTargetRoute
+ }
+ '/redirect/$target/via-beforeLoad': {
+ id: '/redirect/$target/via-beforeLoad'
+ path: '/via-beforeLoad'
+ fullPath: '/redirect/$target/via-beforeLoad'
+ preLoaderRoute: typeof RedirectTargetViaBeforeLoadRouteImport
+ parentRoute: typeof RedirectTargetRoute
+ }
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/api/users/$id': {
+ id: '/api/users/$id'
+ path: '/$id'
+ fullPath: '/api/users/$id'
+ preLoaderRoute: typeof ApiUsersIdRouteImport
+ parentRoute: typeof ApiUsersRoute
+ }
+ '/_layout/_layout-2/layout-b': {
+ id: '/_layout/_layout-2/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/redirect/$target/serverFn/': {
+ id: '/redirect/$target/serverFn/'
+ path: '/serverFn'
+ fullPath: '/redirect/$target/serverFn'
+ preLoaderRoute: typeof RedirectTargetServerFnIndexRouteImport
+ parentRoute: typeof RedirectTargetRoute
+ }
+ '/redirect/$target/serverFn/via-useServerFn': {
+ id: '/redirect/$target/serverFn/via-useServerFn'
+ path: '/serverFn/via-useServerFn'
+ fullPath: '/redirect/$target/serverFn/via-useServerFn'
+ preLoaderRoute: typeof RedirectTargetServerFnViaUseServerFnRouteImport
+ parentRoute: typeof RedirectTargetRoute
+ }
+ '/redirect/$target/serverFn/via-loader': {
+ id: '/redirect/$target/serverFn/via-loader'
+ path: '/serverFn/via-loader'
+ fullPath: '/redirect/$target/serverFn/via-loader'
+ preLoaderRoute: typeof RedirectTargetServerFnViaLoaderRouteImport
+ parentRoute: typeof RedirectTargetRoute
+ }
+ '/redirect/$target/serverFn/via-beforeLoad': {
+ id: '/redirect/$target/serverFn/via-beforeLoad'
+ path: '/serverFn/via-beforeLoad'
+ fullPath: '/redirect/$target/serverFn/via-beforeLoad'
+ preLoaderRoute: typeof RedirectTargetServerFnViaBeforeLoadRouteImport
+ parentRoute: typeof RedirectTargetRoute
+ }
+ '/foo/$bar/$qux/_here': {
+ id: '/foo/$bar/$qux/_here'
+ path: '/foo/$bar/$qux'
+ fullPath: '/foo/$bar/$qux'
+ preLoaderRoute: typeof FooBarQuxHereRouteImport
+ parentRoute: typeof FooBarQuxRoute
+ }
+ '/foo/$bar/$qux/_here/': {
+ id: '/foo/$bar/$qux/_here/'
+ path: '/'
+ fullPath: '/foo/$bar/$qux/'
+ preLoaderRoute: typeof FooBarQuxHereIndexRouteImport
+ parentRoute: typeof FooBarQuxHereRoute
+ }
+ }
+}
+
+interface NotFoundRouteRouteChildren {
+ NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute
+ NotFoundViaHeadRoute: typeof NotFoundViaHeadRoute
+ NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute
+ NotFoundIndexRoute: typeof NotFoundIndexRoute
+}
+
+const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = {
+ NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute,
+ NotFoundViaHeadRoute: NotFoundViaHeadRoute,
+ NotFoundViaLoaderRoute: NotFoundViaLoaderRoute,
+ NotFoundIndexRoute: NotFoundIndexRoute,
+}
+
+const NotFoundRouteRouteWithChildren = NotFoundRouteRoute._addFileChildren(
+ NotFoundRouteRouteChildren,
+)
+
+interface SearchParamsRouteRouteChildren {
+ SearchParamsDefaultRoute: typeof SearchParamsDefaultRoute
+ SearchParamsLoaderThrowsRedirectRoute: typeof SearchParamsLoaderThrowsRedirectRoute
+ SearchParamsIndexRoute: typeof SearchParamsIndexRoute
+}
+
+const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
+ SearchParamsDefaultRoute: SearchParamsDefaultRoute,
+ SearchParamsLoaderThrowsRedirectRoute: SearchParamsLoaderThrowsRedirectRoute,
+ SearchParamsIndexRoute: SearchParamsIndexRoute,
+}
+
+const SearchParamsRouteRouteWithChildren =
+ SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+
+interface LayoutLayout2RouteChildren {
+ LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
+ LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
+}
+
+const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = {
+ LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute,
+ LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute,
+}
+
+const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren(
+ LayoutLayout2RouteChildren,
+)
+
+interface LayoutRouteChildren {
+ LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+ LayoutLayout2Route: LayoutLayout2RouteWithChildren,
+}
+
+const LayoutRouteWithChildren =
+ LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface PostsRouteChildren {
+ PostsPostIdRoute: typeof PostsPostIdRoute
+ PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+ PostsPostIdRoute: PostsPostIdRoute,
+ PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+interface UsersRouteChildren {
+ UsersUserIdRoute: typeof UsersUserIdRoute
+ UsersIndexRoute: typeof UsersIndexRoute
+}
+
+const UsersRouteChildren: UsersRouteChildren = {
+ UsersUserIdRoute: UsersUserIdRoute,
+ UsersIndexRoute: UsersIndexRoute,
+}
+
+const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren)
+
+interface ApiUsersRouteChildren {
+ ApiUsersIdRoute: typeof ApiUsersIdRoute
+}
+
+const ApiUsersRouteChildren: ApiUsersRouteChildren = {
+ ApiUsersIdRoute: ApiUsersIdRoute,
+}
+
+const ApiUsersRouteWithChildren = ApiUsersRoute._addFileChildren(
+ ApiUsersRouteChildren,
+)
+
+interface RedirectTargetRouteChildren {
+ RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute
+ RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute
+ RedirectTargetIndexRoute: typeof RedirectTargetIndexRoute
+ RedirectTargetServerFnViaBeforeLoadRoute: typeof RedirectTargetServerFnViaBeforeLoadRoute
+ RedirectTargetServerFnViaLoaderRoute: typeof RedirectTargetServerFnViaLoaderRoute
+ RedirectTargetServerFnViaUseServerFnRoute: typeof RedirectTargetServerFnViaUseServerFnRoute
+ RedirectTargetServerFnIndexRoute: typeof RedirectTargetServerFnIndexRoute
+}
+
+const RedirectTargetRouteChildren: RedirectTargetRouteChildren = {
+ RedirectTargetViaBeforeLoadRoute: RedirectTargetViaBeforeLoadRoute,
+ RedirectTargetViaLoaderRoute: RedirectTargetViaLoaderRoute,
+ RedirectTargetIndexRoute: RedirectTargetIndexRoute,
+ RedirectTargetServerFnViaBeforeLoadRoute:
+ RedirectTargetServerFnViaBeforeLoadRoute,
+ RedirectTargetServerFnViaLoaderRoute: RedirectTargetServerFnViaLoaderRoute,
+ RedirectTargetServerFnViaUseServerFnRoute:
+ RedirectTargetServerFnViaUseServerFnRoute,
+ RedirectTargetServerFnIndexRoute: RedirectTargetServerFnIndexRoute,
+}
+
+const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren(
+ RedirectTargetRouteChildren,
+)
+
+interface FooBarQuxHereRouteChildren {
+ FooBarQuxHereIndexRoute: typeof FooBarQuxHereIndexRoute
+}
+
+const FooBarQuxHereRouteChildren: FooBarQuxHereRouteChildren = {
+ FooBarQuxHereIndexRoute: FooBarQuxHereIndexRoute,
+}
+
+const FooBarQuxHereRouteWithChildren = FooBarQuxHereRoute._addFileChildren(
+ FooBarQuxHereRouteChildren,
+)
+
+interface FooBarQuxRouteChildren {
+ FooBarQuxHereRoute: typeof FooBarQuxHereRouteWithChildren
+}
+
+const FooBarQuxRouteChildren: FooBarQuxRouteChildren = {
+ FooBarQuxHereRoute: FooBarQuxHereRouteWithChildren,
+}
+
+const FooBarQuxRouteWithChildren = FooBarQuxRoute._addFileChildren(
+ FooBarQuxRouteChildren,
+)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
+ SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ LayoutRoute: LayoutRouteWithChildren,
+ DeferredRoute: DeferredRoute,
+ HydratedRoute: HydratedRoute,
+ InlineScriptsRoute: InlineScriptsRoute,
+ LinksRoute: LinksRoute,
+ PostsRoute: PostsRouteWithChildren,
+ ScriptsRoute: ScriptsRoute,
+ StaticRoute: StaticRoute,
+ StreamRoute: StreamRoute,
+ UsersRoute: UsersRouteWithChildren,
+ Char45824Char54620Char48124Char44397Route:
+ Char45824Char54620Char48124Char44397Route,
+ ApiUsersRoute: ApiUsersRouteWithChildren,
+ MultiCookieRedirectTargetRoute: MultiCookieRedirectTargetRoute,
+ RedirectTargetRoute: RedirectTargetRouteWithChildren,
+ MultiCookieRedirectIndexRoute: MultiCookieRedirectIndexRoute,
+ RedirectIndexRoute: RedirectIndexRoute,
+ PostsPostIdDeepRoute: PostsPostIdDeepRoute,
+ FooBarQuxRoute: FooBarQuxRouteWithChildren,
+}
+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/basic-hydrate-false/src/router.tsx b/e2e/react-start/basic-hydrate-false/src/router.tsx
new file mode 100644
index 00000000000..fef35c9e067
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
+import { NotFound } from './components/NotFound'
+
+export function getRouter() {
+ const router = createRouter({
+ routeTree,
+ scrollRestoration: true,
+ defaultPreload: 'intent',
+ defaultErrorComponent: DefaultCatchBoundary,
+ defaultNotFoundComponent: () => ,
+ })
+
+ return router
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/__root.tsx b/e2e/react-start/basic-hydrate-false/src/routes/__root.tsx
new file mode 100644
index 00000000000..c7d87cad188
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/__root.tsx
@@ -0,0 +1,188 @@
+///
+import * as React from 'react'
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+import { seo } from '~/utils/seo'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ charSet: 'utf-8',
+ },
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ ...seo({
+ title:
+ 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework',
+ description: `TanStack Start is a type-safe, client-first, full-stack React framework. `,
+ }),
+ ],
+ links: [
+ { rel: 'stylesheet', href: appCss },
+ {
+ rel: 'apple-touch-icon',
+ sizes: '180x180',
+ href: '/apple-touch-icon.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '32x32',
+ href: '/favicon-32x32.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '16x16',
+ href: '/favicon-16x16.png',
+ },
+ { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
+ { rel: 'icon', href: '/favicon.ico' },
+ ],
+ styles: [
+ {
+ media: 'all and (min-width: 500px)',
+ children: `
+ .inline-div {
+ color: white;
+ background-color: gray;
+ max-width: 250px;
+ }`,
+ },
+ ],
+ }),
+ errorComponent: (props) => {
+ return (
+
+
+
+ )
+ },
+ notFoundComponent: () => ,
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+ )
+}
+
+const RouterDevtools =
+ process.env.NODE_ENV === 'production'
+ ? () => null // Render nothing in production
+ : React.lazy(() =>
+ // Lazy load in development
+ import('@tanstack/react-router-devtools').then((res) => ({
+ default: res.TanStackRouterDevtools,
+ })),
+ )
+
+function RootDocument({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Users
+ {' '}
+
+ Layout
+ {' '}
+
+ Scripts
+ {' '}
+
+ Inline Scripts
+ {' '}
+
+ Deferred
+ {' '}
+
+ redirect
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+ {children}
+ This is an inline styled div
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/_layout.tsx b/e2e/react-start/basic-hydrate-false/src/routes/_layout.tsx
new file mode 100644
index 00000000000..02ddbb1cd94
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_layout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2.tsx b/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2.tsx
new file mode 100644
index 00000000000..3b7dbf29031
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2/layout-a.tsx
new file mode 100644
index 00000000000..61e19b4d9f1
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2/layout-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2/layout-b.tsx
new file mode 100644
index 00000000000..cceed1fb9ad
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/_layout/_layout-2/layout-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/api.users.ts b/e2e/react-start/basic-hydrate-false/src/routes/api.users.ts
new file mode 100644
index 00000000000..a03076490b8
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/api.users.ts
@@ -0,0 +1,28 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { json } from '@tanstack/react-start'
+import axios from 'redaxios'
+
+import type { User } from '~/utils/users'
+
+let queryURL = 'https://jsonplaceholder.typicode.com'
+
+if (import.meta.env.VITE_NODE_ENV === 'test') {
+ queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}`
+}
+
+export const Route = createFileRoute('/api/users')({
+ server: {
+ handlers: {
+ GET: async ({ request }) => {
+ console.info('Fetching users... @', request.url)
+ const res = await axios.get>(`${queryURL}/users`)
+
+ const list = res.data.slice(0, 10)
+
+ return json(
+ list.map((u) => ({ id: u.id, name: u.name, email: u.email })),
+ )
+ },
+ },
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/api/users.$id.ts b/e2e/react-start/basic-hydrate-false/src/routes/api/users.$id.ts
new file mode 100644
index 00000000000..2d2c279e931
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/api/users.$id.ts
@@ -0,0 +1,32 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { json } from '@tanstack/react-start'
+import axios from 'redaxios'
+import type { User } from '~/utils/users'
+
+let queryURL = 'https://jsonplaceholder.typicode.com'
+
+if (import.meta.env.VITE_NODE_ENV === 'test') {
+ queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}`
+}
+
+export const Route = createFileRoute('/api/users/$id')({
+ server: {
+ handlers: {
+ GET: async ({ request, params }) => {
+ console.info(`Fetching users by id=${params.id}... @`, request.url)
+ try {
+ const res = await axios.get(`${queryURL}/users/` + params.id)
+
+ return json({
+ id: res.data.id,
+ name: res.data.name,
+ email: res.data.email,
+ })
+ } catch (e) {
+ console.error(e)
+ return json({ error: 'User not found' }, { status: 404 })
+ }
+ },
+ },
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/deferred.tsx b/e2e/react-start/basic-hydrate-false/src/routes/deferred.tsx
new file mode 100644
index 00000000000..9c6e3064b88
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/deferred.tsx
@@ -0,0 +1,62 @@
+import { Await, createFileRoute } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+import { Suspense, useState } from 'react'
+
+const personServerFn = createServerFn({ method: 'GET' })
+ .inputValidator((data: { name: string }) => data)
+ .handler(({ data }) => {
+ return { name: data.name, randomNumber: Math.floor(Math.random() * 100) }
+ })
+
+const slowServerFn = createServerFn({ method: 'GET' })
+ .inputValidator((data: { name: string }) => data)
+ .handler(async ({ data }) => {
+ await new Promise((r) => setTimeout(r, 1000))
+ return { name: data.name, randomNumber: Math.floor(Math.random() * 100) }
+ })
+
+export const Route = createFileRoute('/deferred')({
+ loader: async () => {
+ return {
+ deferredStuff: new Promise((r) =>
+ setTimeout(() => r('Hello deferred!'), 2000),
+ ),
+ deferredPerson: slowServerFn({ data: { name: 'Tanner Linsley' } }),
+ person: await personServerFn({ data: { name: 'John Doe' } }),
+ }
+ },
+ component: Deferred,
+})
+
+function Deferred() {
+ const [count, setCount] = useState(0)
+ const { deferredStuff, deferredPerson, person } = Route.useLoaderData()
+
+ return (
+
+
+ {person.name} - {person.randomNumber}
+
+
Loading person... }>
+ (
+
+ {data.name} - {data.randomNumber}
+
+ )}
+ />
+
+ Loading stuff...}>
+ {data} }
+ />
+
+ Count: {count}
+
+ setCount(count + 1)}>Increment
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/foo/$bar/$qux/_here.tsx b/e2e/react-start/basic-hydrate-false/src/routes/foo/$bar/$qux/_here.tsx
new file mode 100644
index 00000000000..95a5599e237
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/foo/$bar/$qux/_here.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/$bar/$qux/_here')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a deeper layout with parameters
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/foo/$bar/$qux/_here/index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/foo/$bar/$qux/_here/index.tsx
new file mode 100644
index 00000000000..924c8bb3c16
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/foo/$bar/$qux/_here/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/$bar/$qux/_here/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return OK you got me
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/hydrated.tsx b/e2e/react-start/basic-hydrate-false/src/routes/hydrated.tsx
new file mode 100644
index 00000000000..8d99d83ca48
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/hydrated.tsx
@@ -0,0 +1,114 @@
+import * as React from 'react'
+import { createFileRoute, Link } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/hydrated')({
+ // Default is hydrate: true, so this is a normal React route
+ loader: () => {
+ return {
+ serverTime: new Date().toISOString(),
+ message: 'This data was loaded on the server',
+ }
+ },
+ head: () => ({
+ meta: [
+ {
+ title: 'Hydrated Route - TanStack Router Demo',
+ },
+ {
+ name: 'description',
+ content: 'A fully interactive React page with hydration',
+ },
+ ],
+ }),
+ component: HydratedComponent,
+})
+
+function HydratedComponent() {
+ const data = Route.useLoaderData()
+ const [count, setCount] = React.useState(0)
+ const [mounted, setMounted] = React.useState(false)
+
+ React.useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ return (
+
+
+ ← Back to Home
+
+
+
✅ Hydrated Route
+
+
+
+
✨ Interactive Features
+
+
+
Counter (React State):
+
+ setCount(count - 1)}
+ className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
+ >
+ -
+
+
+ {count}
+
+ setCount(count + 1)}
+ className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
+ >
+ +
+
+
+
+
+
+
Hydration Status:
+
+ {mounted ? '✅ Hydrated and Interactive' : '⏳ Server-Rendered'}
+
+
+
+
+
+
📦 Server Data
+
+
+ Server Time: {' '}
+
+ {data.serverTime}
+
+
+
+ Message: {data.message}
+
+
+
+
+
+
🔍 What's Included
+
+ Open DevTools → Network → JS to see the loaded bundles:
+
+
+ React runtime bundle
+ React DOM bundle
+ TanStack Router bundle
+ Your application code
+ Hydration data (window.$_TSR)
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/index.tsx
new file mode 100644
index 00000000000..743b1c99afa
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/index.tsx
@@ -0,0 +1,64 @@
+import * as React from 'react'
+import { createFileRoute, Link } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
+ TanStack Router - Hydrate Feature Demo
+
+
+
+
+ This demo showcases the new hydrate option
+ for TanStack Start routes.
+
+
+ The hydrate: false option allows you to
+ render pages that are server-side rendered but do not include the React hydration bundle.
+
+
+
+
+
+
+ ✅ Hydrated Route
+
+
+ A normal React route with full interactivity. Includes the React bundle and hydration.
+
+
+
+
+
+ 🚫 Static Route (hydrate: false)
+
+
+ A server-rendered static page. No React bundle, no hydration, no interactivity.
+
+
+
+
+
+
💡 Use Cases
+
+ Marketing/landing pages that don't need interactivity
+ Legal pages (Terms, Privacy Policy)
+ Blog posts or documentation
+ SEO-focused content pages
+ Reducing JavaScript bundle size for static content
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/inline-scripts.tsx b/e2e/react-start/basic-hydrate-false/src/routes/inline-scripts.tsx
new file mode 100644
index 00000000000..86255c63bd4
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/inline-scripts.tsx
@@ -0,0 +1,30 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/inline-scripts')({
+ head: () => ({
+ scripts: [
+ {
+ children:
+ 'window.INLINE_SCRIPT_1 = true; console.log("Inline script 1 executed");',
+ },
+ {
+ children:
+ 'window.INLINE_SCRIPT_2 = "test"; console.log("Inline script 2 executed");',
+ type: 'text/javascript',
+ },
+ ],
+ }),
+ component: InlineScriptsComponent,
+})
+
+function InlineScriptsComponent() {
+ return (
+
+
Inline Scripts Test
+
+ This route tests inline script duplication prevention. Two inline
+ scripts should be loaded.
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/links.tsx b/e2e/react-start/basic-hydrate-false/src/routes/links.tsx
new file mode 100644
index 00000000000..adc8fe9c4d7
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/links.tsx
@@ -0,0 +1,47 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/links')({
+ component: () => {
+ const navigate = Route.useNavigate()
+ return (
+
+
+ link test
+
+
+
+ Link to /posts
+
+
+
+
+ Link to /posts (reloadDocument=true)
+
+
+
+ navigate({ to: '/posts' })}>
+ navigate to /posts
+
+
+
+ navigate({ to: '/posts', reloadDocument: true })}
+ >
+ navigate to /posts (reloadDocument=true)
+
+
+
+ )
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/multi-cookie-redirect/index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/multi-cookie-redirect/index.tsx
new file mode 100644
index 00000000000..7ae4947aef1
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/multi-cookie-redirect/index.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+import { setCookie } from '@tanstack/react-start/server'
+
+const setMultipleCookiesAndRedirect = createServerFn().handler(() => {
+ // Set multiple cookies before redirecting
+ // This tests that multiple Set-Cookie headers are preserved during redirect
+ setCookie('session', 'session-value', { path: '/' })
+ setCookie('csrf', 'csrf-token-value', { path: '/' })
+ setCookie('theme', 'dark', { path: '/' })
+
+ throw redirect({ to: '/multi-cookie-redirect/target' })
+})
+
+export const Route = createFileRoute('/multi-cookie-redirect/')({
+ loader: () => setMultipleCookiesAndRedirect(),
+ component: () => null,
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/multi-cookie-redirect/target.tsx b/e2e/react-start/basic-hydrate-false/src/routes/multi-cookie-redirect/target.tsx
new file mode 100644
index 00000000000..3b0cb20bef1
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/multi-cookie-redirect/target.tsx
@@ -0,0 +1,39 @@
+import { createFileRoute } from '@tanstack/react-router'
+import Cookies from 'js-cookie'
+import React, { useEffect } from 'react'
+
+export const Route = createFileRoute('/multi-cookie-redirect/target')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const [cookies, setCookies] = React.useState>({})
+
+ useEffect(() => {
+ setCookies({
+ session: Cookies.get('session') || '',
+ csrf: Cookies.get('csrf') || '',
+ theme: Cookies.get('theme') || '',
+ })
+ }, [])
+
+ return (
+
+
+ Multi Cookie Redirect Target
+
+
+
+ Session cookie:{' '}
+ {cookies.session}
+
+
+ CSRF cookie: {cookies.csrf}
+
+
+ Theme cookie: {cookies.theme}
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/not-found/index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/not-found/index.tsx
new file mode 100644
index 00000000000..722813fca36
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/not-found/index.tsx
@@ -0,0 +1,41 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/not-found/')({
+ component: () => {
+ const preload = Route.useSearch({ select: (s) => s.preload })
+ return (
+
+
+
+ via-beforeLoad
+
+
+
+
+ via-loader
+
+
+
+
+ via-head
+
+
+
+ )
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/not-found/route.tsx b/e2e/react-start/basic-hydrate-false/src/routes/not-found/route.tsx
new file mode 100644
index 00000000000..e604e098fd0
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/not-found/route.tsx
@@ -0,0 +1,8 @@
+import { createFileRoute } from '@tanstack/react-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/not-found')({
+ validateSearch: z.object({
+ preload: z.literal(false).optional(),
+ }),
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-beforeLoad.tsx b/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-beforeLoad.tsx
new file mode 100644
index 00000000000..85164dbab5b
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-beforeLoad.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/not-found/via-beforeLoad')({
+ beforeLoad: () => {
+ throw notFound()
+ },
+ component: RouteComponent,
+ notFoundComponent: () => {
+ return (
+
+ Not Found "/not-found/via-beforeLoad"!
+
+ )
+ },
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/not-found/via-beforeLoad"!
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-head.tsx b/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-head.tsx
new file mode 100644
index 00000000000..7cd09f9fa31
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-head.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/not-found/via-head')({
+ head: () => {
+ throw notFound()
+ },
+ component: RouteComponent,
+ notFoundComponent: () => {
+ return (
+
+ Not Found "/not-found/via-head"!
+
+ )
+ },
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/not-found/via-head"!
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-loader.tsx b/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-loader.tsx
new file mode 100644
index 00000000000..6174b27f775
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/not-found/via-loader.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/not-found/via-loader')({
+ loader: () => {
+ throw notFound()
+ },
+ component: RouteComponent,
+ notFoundComponent: () => {
+ return (
+
+ Not Found "/not-found/via-loader"!
+
+ )
+ },
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/not-found/via-loader"!
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/posts.$postId.tsx b/e2e/react-start/basic-hydrate-false/src/routes/posts.$postId.tsx
new file mode 100644
index 00000000000..09d00685829
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/posts.$postId.tsx
@@ -0,0 +1,39 @@
+import { ErrorComponent, Link, createFileRoute } from '@tanstack/react-router'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+
+import { fetchPost } from '~/utils/posts'
+import { NotFound } from '~/components/NotFound'
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost({ data: postId }),
+ errorComponent: PostErrorComponent,
+ component: PostComponent,
+ notFoundComponent: () => {
+ return Post not found
+ },
+})
+
+function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
{post.title}
+
{post.body}
+
+ Deep View
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/posts.index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/posts.index.tsx
new file mode 100644
index 00000000000..1bad79b0f7c
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/posts.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/posts.tsx b/e2e/react-start/basic-hydrate-false/src/routes/posts.tsx
new file mode 100644
index 00000000000..0f69c183419
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/posts.tsx
@@ -0,0 +1,46 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+
+import { fetchPosts } from '~/utils/posts'
+
+export const Route = createFileRoute('/posts')({
+ head: () => ({
+ meta: [
+ {
+ title: 'Posts page',
+ },
+ ],
+ }),
+ loader: async () => fetchPosts(),
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/posts_.$postId.deep.tsx b/e2e/react-start/basic-hydrate-false/src/routes/posts_.$postId.deep.tsx
new file mode 100644
index 00000000000..1f785f5f7ff
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/posts_.$postId.deep.tsx
@@ -0,0 +1,30 @@
+import { ErrorComponent, Link, createFileRoute } from '@tanstack/react-router'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+import { fetchPost } from '~/utils/posts'
+
+export const Route = createFileRoute('/posts_/$postId/deep')({
+ loader: async ({ params: { postId } }) => fetchPost({ data: postId }),
+ errorComponent: PostDeepErrorComponent,
+ component: PostDeepComponent,
+})
+
+function PostDeepErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+function PostDeepComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
+ ← All Posts
+
+
{post.title}
+
{post.body}
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target.tsx
new file mode 100644
index 00000000000..686f1c7056a
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute, retainSearchParams } from '@tanstack/react-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/redirect/$target')({
+ params: {
+ parse: (p) =>
+ z
+ .object({
+ target: z.union([z.literal('internal'), z.literal('external')]),
+ })
+ .parse(p),
+ },
+ validateSearch: z.object({
+ reloadDocument: z.boolean().optional(),
+ preload: z.literal(false).optional(),
+ externalHost: z.string().optional(),
+ }),
+ search: {
+ middlewares: [retainSearchParams(['externalHost'])],
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/index.tsx
new file mode 100644
index 00000000000..f399d965cf1
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/index.tsx
@@ -0,0 +1,76 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/')({
+ component: () => {
+ const preload = Route.useSearch({ select: (s) => s.preload })
+ return (
+
+
+
+ via-beforeLoad
+
+
+
+
+ via-beforeLoad (reloadDocument=true)
+
+
+
+
+ via-loader
+
+
+
+
+ via-loader (reloadDocument=true)
+
+
+
+
+ serverFn
+
+
+
+ )
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/index.tsx
new file mode 100644
index 00000000000..ddb0d8e9964
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/index.tsx
@@ -0,0 +1,86 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/serverFn/')({
+ component: () => (
+
+
+ redirect test with server functions (target {Route.useParams().target})
+
+
+
+ via-beforeLoad
+
+
+
+
+ via-beforeLoad (reloadDocument=true)
+
+
+
+
+ via-loader
+
+
+
+
+ via-loader (reloadDocument=true)
+
+
+
+
+ via-useServerFn
+
+
+
+
+ via-useServerFn (reloadDocument=true)
+
+
+
+ ),
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-beforeLoad.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-beforeLoad.tsx
new file mode 100644
index 00000000000..eed26559f34
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-beforeLoad.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { throwRedirect } from '~/components/throwRedirect'
+
+export const Route = createFileRoute(
+ '/redirect/$target/serverFn/via-beforeLoad',
+)({
+ beforeLoad: ({
+ params: { target },
+ search: { reloadDocument, externalHost },
+ }) => throwRedirect({ data: { target, reloadDocument, externalHost } }),
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-loader.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-loader.tsx
new file mode 100644
index 00000000000..1db205e3115
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-loader.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { throwRedirect } from '~/components/throwRedirect'
+
+export const Route = createFileRoute('/redirect/$target/serverFn/via-loader')({
+ loaderDeps: ({ search: { reloadDocument, externalHost } }) => ({
+ reloadDocument,
+ externalHost,
+ }),
+ loader: ({ params: { target }, deps: { reloadDocument, externalHost } }) =>
+ throwRedirect({ data: { target, reloadDocument, externalHost } }),
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-useServerFn.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-useServerFn.tsx
new file mode 100644
index 00000000000..866bb19b10e
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/serverFn/via-useServerFn.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { RedirectOnClick } from '~/components/RedirectOnClick'
+
+export const Route = createFileRoute(
+ '/redirect/$target/serverFn/via-useServerFn',
+)({
+ component: () => {
+ const { target } = Route.useParams()
+ const { reloadDocument, externalHost } = Route.useSearch()
+ return (
+
+ )
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/via-beforeLoad.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/via-beforeLoad.tsx
new file mode 100644
index 00000000000..3b30323869a
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/via-beforeLoad.tsx
@@ -0,0 +1,16 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({
+ beforeLoad: ({
+ params: { target },
+ search: { reloadDocument, externalHost },
+ }) => {
+ switch (target) {
+ case 'internal':
+ throw redirect({ to: '/posts', reloadDocument })
+ case 'external':
+ throw redirect({ href: externalHost })
+ }
+ },
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/via-loader.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/via-loader.tsx
new file mode 100644
index 00000000000..c88a3e66f10
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/$target/via-loader.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/via-loader')({
+ loaderDeps: ({ search: { reloadDocument, externalHost } }) => ({
+ reloadDocument,
+ externalHost,
+ }),
+ loader: ({ params: { target }, deps: { externalHost, reloadDocument } }) => {
+ switch (target) {
+ case 'internal':
+ throw redirect({ to: '/posts', reloadDocument })
+ case 'external':
+ throw redirect({ href: externalHost })
+ }
+ },
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/redirect/index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/redirect/index.tsx
new file mode 100644
index 00000000000..c0b26a1df4f
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/redirect/index.tsx
@@ -0,0 +1,28 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/')({
+ component: () => (
+
+
+ internal
+ {' '}
+
+ external
+
+
+ ),
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/scripts.tsx b/e2e/react-start/basic-hydrate-false/src/routes/scripts.tsx
new file mode 100644
index 00000000000..a2b613bc3c6
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/scripts.tsx
@@ -0,0 +1,31 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+const isProd = import.meta.env.PROD
+
+export const Route = createFileRoute('/scripts')({
+ head: () => ({
+ scripts: [
+ {
+ src: 'script.js',
+ },
+ isProd
+ ? undefined
+ : {
+ src: 'script2.js',
+ },
+ ],
+ }),
+ component: ScriptsComponent,
+})
+
+function ScriptsComponent() {
+ return (
+
+
Scripts Test
+
+ Both `script.js` and `script2.js` are included in development, but only
+ `script.js` is included in production.
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/search-params/default.tsx b/e2e/react-start/basic-hydrate-false/src/routes/search-params/default.tsx
new file mode 100644
index 00000000000..19e1149b8d2
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/search-params/default.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { z } from 'zod'
+
+export const Route = createFileRoute('/search-params/default')({
+ validateSearch: z.object({
+ default: z.string().default('d1'),
+ }),
+ beforeLoad: ({ context }) => {
+ if (context.hello !== 'world') {
+ throw new Error('Context hello is not "world"')
+ }
+ },
+ loader: ({ context }) => {
+ if (context.hello !== 'world') {
+ throw new Error('Context hello is not "world"')
+ }
+ },
+ component: () => {
+ const search = Route.useSearch()
+ const context = Route.useRouteContext()
+ return (
+ <>
+ {search.default}
+ {context.hello}
+ >
+ )
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/search-params/index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/search-params/index.tsx
new file mode 100644
index 00000000000..c0d4a55ac85
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/search-params/index.tsx
@@ -0,0 +1,26 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/search-params/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+ go to /search-params/default
+
+
+
+ go to /search-params/default?default=d2
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/search-params/loader-throws-redirect.tsx b/e2e/react-start/basic-hydrate-false/src/routes/search-params/loader-throws-redirect.tsx
new file mode 100644
index 00000000000..c55628ad331
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/search-params/loader-throws-redirect.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { z } from 'zod'
+
+export const Route = createFileRoute('/search-params/loader-throws-redirect')({
+ validateSearch: z.object({
+ step: z.enum(['a', 'b', 'c']).optional(),
+ }),
+ loaderDeps: ({ search: { step } }) => ({ step }),
+ loader: ({ deps: { step } }) => {
+ if (step === undefined) {
+ throw redirect({
+ to: '/search-params/loader-throws-redirect',
+ search: { step: 'a' },
+ })
+ }
+ },
+ component: () => {
+ const search = Route.useSearch()
+ return (
+
+
SearchParams
+
{search.step}
+
+ )
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/search-params/route.tsx b/e2e/react-start/basic-hydrate-false/src/routes/search-params/route.tsx
new file mode 100644
index 00000000000..5adb281c41b
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/search-params/route.tsx
@@ -0,0 +1,8 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/search-params')({
+ beforeLoad: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ return { hello: 'world' as string }
+ },
+})
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/static.tsx b/e2e/react-start/basic-hydrate-false/src/routes/static.tsx
new file mode 100644
index 00000000000..73c6651bc21
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/static.tsx
@@ -0,0 +1,162 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/static')({
+ // This is the key: hydrate: false means no React hydration
+ hydrate: false,
+ loader: () => {
+ return {
+ serverTime: new Date().toISOString(),
+ message: 'This data was loaded on the server',
+ pageViews: Math.floor(Math.random() * 10000),
+ }
+ },
+ head: () => ({
+ meta: [
+ {
+ title: 'Static Route - TanStack Router Demo',
+ },
+ {
+ name: 'description',
+ content: 'A static server-rendered page with no hydration',
+ },
+ ],
+ // You can still include external scripts that don't require React
+ scripts: [
+ {
+ children: `
+ console.log('✅ External scripts still work!');
+ console.log('This page has no React hydration');
+ `,
+ },
+ ],
+ }),
+ component: StaticComponent,
+})
+
+function StaticComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+
+ ← Back to Home
+
+
+
+ 🚫 Static Route (hydrate: false)
+
+
+
+
+
🎯 What This Means
+
+
+ ✅
+ Page is server-side rendered
+
+
+ ✅
+ All content is SEO-friendly
+
+
+ ✅
+ Fast initial page load (no JS bundle)
+
+
+ ❌
+ No React interactivity
+
+
+ ❌
+ No useState, useEffect, or event handlers
+
+
+ ❌
+ Links are traditional navigation (full page reload)
+
+
+
+
+
+
📦 Server Data
+
+
+ Server Time: {' '}
+
+ {data.serverTime}
+
+
+
+ Message: {data.message}
+
+
+ Page Views: {' '}
+ {data.pageViews.toLocaleString()}
+
+
+
+
+
+
+ ⚠️ Try Clicking This Button
+
+
+ This button has an onClick handler, but it won't work because React
+ is not hydrated:
+
+
alert('This will NOT work!')}
+ className="px-6 py-3 bg-gray-300 text-gray-600 rounded cursor-not-allowed"
+ data-testid="inactive-button"
+ >
+ Inactive Button (onClick won't fire)
+
+
+ Nothing happens when you click because there's no JavaScript event
+ handler attached.
+
+
+
+
+
🔍 What's NOT Included
+
Open DevTools → Network → JS. You'll see:
+
+
+ React runtime bundle (NOT loaded)
+
+
+ React DOM bundle (NOT loaded)
+
+
+ TanStack Router client bundle (NOT loaded)
+
+
+ Your application code (NOT loaded)
+
+
+ Hydration data (NOT included)
+
+
+
+ ✅ Result: Significantly smaller page size and faster initial load!
+
+
+
+
+
💡 Perfect For:
+
+ Terms of Service / Privacy Policy pages
+ Blog posts and articles
+ Marketing landing pages
+ Documentation
+ Any content that doesn't need interactivity
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/stream.tsx b/e2e/react-start/basic-hydrate-false/src/routes/stream.tsx
new file mode 100644
index 00000000000..691792ac214
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/stream.tsx
@@ -0,0 +1,64 @@
+import { Await, createFileRoute } from '@tanstack/react-router'
+import { useEffect, useState } from 'react'
+
+export const Route = createFileRoute('/stream')({
+ component: Home,
+ loader() {
+ return {
+ promise: new Promise((resolve) =>
+ setTimeout(() => resolve('promise-data'), 150),
+ ),
+ stream: new ReadableStream({
+ async start(controller) {
+ for (let i = 0; i < 5; i++) {
+ await new Promise((resolve) => setTimeout(resolve, 200))
+ controller.enqueue(`stream-data-${i} `)
+ }
+ controller.close()
+ },
+ }),
+ }
+ },
+})
+
+const decoder = new TextDecoder('utf-8')
+
+function Home() {
+ const { promise, stream } = Route.useLoaderData()
+ const [streamData, setStreamData] = useState>([])
+
+ useEffect(() => {
+ async function fetchStream() {
+ const reader = stream.getReader()
+ let chunk
+
+ while (!(chunk = await reader.read()).done) {
+ let value = chunk.value
+ if (typeof value !== 'string') {
+ value = decoder.decode(value, { stream: !chunk.done })
+ }
+ setStreamData((prev) => [...prev, value])
+ }
+ }
+
+ fetchStream()
+ }, [])
+
+ return (
+ <>
+ (
+
+ {promiseData}
+
+ {streamData.map((d) => (
+
{d}
+ ))}
+
+
+ )}
+ />
+ >
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/users.$userId.tsx b/e2e/react-start/basic-hydrate-false/src/routes/users.$userId.tsx
new file mode 100644
index 00000000000..59ddc3df873
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/users.$userId.tsx
@@ -0,0 +1,39 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/react-router'
+import axios from 'redaxios'
+import { getRouterInstance } from '@tanstack/react-start'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+
+import type { User } from '~/utils/users'
+import { NotFound } from '~/components/NotFound'
+
+export const Route = createFileRoute('/users/$userId')({
+ loader: async ({ params: { userId } }) => {
+ const router = await getRouterInstance()
+ return await axios
+ .get('/api/users/' + userId, { baseURL: router.options.origin })
+ .then((r) => r.data)
+ .catch(() => {
+ throw new Error('Failed to fetch user')
+ })
+ },
+ errorComponent: UserErrorComponent,
+ component: UserComponent,
+ notFoundComponent: () => {
+ return User not found
+ },
+})
+
+function UserErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+function UserComponent() {
+ const user = Route.useLoaderData()
+
+ return (
+
+
{user.name}
+
{user.email}
+
+ )
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/users.index.tsx b/e2e/react-start/basic-hydrate-false/src/routes/users.index.tsx
new file mode 100644
index 00000000000..b6b0ee67fbf
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/users.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/users/')({
+ component: UsersIndexComponent,
+})
+
+function UsersIndexComponent() {
+ return Select a user.
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/routes/users.tsx b/e2e/react-start/basic-hydrate-false/src/routes/users.tsx
new file mode 100644
index 00000000000..f7a57210971
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/routes/users.tsx
@@ -0,0 +1,50 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+import { getRouterInstance } from '@tanstack/react-start'
+import axios from 'redaxios'
+
+import type { User } from '~/utils/users'
+
+export const Route = createFileRoute('/users')({
+ loader: async () => {
+ const router = await getRouterInstance()
+ return await axios
+ .get>('/api/users', { baseURL: router.options.origin })
+ .then((r) => r.data)
+ .catch(() => {
+ throw new Error('Failed to fetch users')
+ })
+ },
+ component: UsersComponent,
+})
+
+function UsersComponent() {
+ const users = Route.useLoaderData()
+
+ return (
+
+
+ {[
+ ...users,
+ { id: 'i-do-not-exist', name: 'Non-existent User', email: '' },
+ ].map((user) => {
+ return (
+
+
+ {user.name}
+
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git "a/e2e/react-start/basic-hydrate-false/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/react-start/basic-hydrate-false/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
new file mode 100644
index 00000000000..c70cb5096a9
--- /dev/null
+++ "b/e2e/react-start/basic-hydrate-false/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/대한민국')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/대한민국"!
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/server.ts b/e2e/react-start/basic-hydrate-false/src/server.ts
new file mode 100644
index 00000000000..00d13b4d178
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/server.ts
@@ -0,0 +1,11 @@
+// DO NOT DELETE THIS FILE!!!
+// This file is a good smoke test to make sure the custom server entry is working
+import handler from '@tanstack/react-start/server-entry'
+
+console.log("[server-entry]: using custom server entry in 'src/server.ts'")
+
+export default {
+ fetch(request: Request) {
+ return handler.fetch(request)
+ },
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/styles/app.css b/e2e/react-start/basic-hydrate-false/src/styles/app.css
new file mode 100644
index 00000000000..c36c737cd46
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/styles/app.css
@@ -0,0 +1,30 @@
+@import 'tailwindcss';
+
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+@layer base {
+ html {
+ color-scheme: light dark;
+ }
+
+ * {
+ @apply border-gray-200 dark:border-gray-800;
+ }
+
+ html,
+ body {
+ @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200;
+ }
+
+ .using-mouse * {
+ outline: none !important;
+ }
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/utils/posts.tsx b/e2e/react-start/basic-hydrate-false/src/utils/posts.tsx
new file mode 100644
index 00000000000..b2d9f3edecf
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/utils/posts.tsx
@@ -0,0 +1,42 @@
+import { notFound } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+import axios from 'redaxios'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+let queryURL = 'https://jsonplaceholder.typicode.com'
+
+if (import.meta.env.VITE_NODE_ENV === 'test') {
+ queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}`
+}
+
+export const fetchPost = createServerFn({ method: 'GET' })
+ .inputValidator((postId: string) => postId)
+ .handler(async ({ data: postId }) => {
+ console.info(`Fetching post with id ${postId}...`)
+ const post = await axios
+ .get(`${queryURL}/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ console.error(err)
+ if (err.status === 404) {
+ throw notFound()
+ }
+ throw err
+ })
+
+ return post
+ })
+
+export const fetchPosts = createServerFn({ method: 'GET' }).handler(
+ async () => {
+ console.info('Fetching posts...')
+ return axios
+ .get>(`${queryURL}/posts`)
+ .then((r) => r.data.slice(0, 10))
+ },
+)
diff --git a/e2e/react-start/basic-hydrate-false/src/utils/seo.ts b/e2e/react-start/basic-hydrate-false/src/utils/seo.ts
new file mode 100644
index 00000000000..d18ad84b74e
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/utils/seo.ts
@@ -0,0 +1,33 @@
+export const seo = ({
+ title,
+ description,
+ keywords,
+ image,
+}: {
+ title: string
+ description?: string
+ image?: string
+ keywords?: string
+}) => {
+ const tags = [
+ { title },
+ { name: 'description', content: description },
+ { name: 'keywords', content: keywords },
+ { name: 'twitter:title', content: title },
+ { name: 'twitter:description', content: description },
+ { name: 'twitter:creator', content: '@tannerlinsley' },
+ { name: 'twitter:site', content: '@tannerlinsley' },
+ { name: 'og:type', content: 'website' },
+ { name: 'og:title', content: title },
+ { name: 'og:description', content: description },
+ ...(image
+ ? [
+ { name: 'twitter:image', content: image },
+ { name: 'twitter:card', content: 'summary_large_image' },
+ { name: 'og:image', content: image },
+ ]
+ : []),
+ ]
+
+ return tags
+}
diff --git a/e2e/react-start/basic-hydrate-false/src/utils/users.tsx b/e2e/react-start/basic-hydrate-false/src/utils/users.tsx
new file mode 100644
index 00000000000..46be4b15804
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/src/utils/users.tsx
@@ -0,0 +1,9 @@
+export type User = {
+ id: number
+ name: string
+ email: string
+}
+
+const PORT = process.env.VITE_SERVER_PORT || 3000
+
+export const DEPLOY_URL = `http://localhost:${PORT}`
diff --git a/e2e/react-start/basic-hydrate-false/tests/hydrate.spec.ts b/e2e/react-start/basic-hydrate-false/tests/hydrate.spec.ts
new file mode 100644
index 00000000000..1630ffb715f
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/hydrate.spec.ts
@@ -0,0 +1,130 @@
+import { expect, test } from '@playwright/test'
+
+test.describe('Hydrate: false feature', () => {
+ test('should not include main bundle scripts when hydrate is false', async ({
+ page,
+ }) => {
+ // Visit the route with hydrate: false
+ await page.goto('/static')
+
+ // Wait for the page to load
+ await expect(page.getByTestId('static-heading')).toBeVisible()
+
+ // Get the HTML content
+ const html = await page.content()
+
+ // The main bundle scripts from the manifest should NOT be present
+ // These are the scripts that would hydrate the React app
+ const hasMainBundleScript = html.includes('type="module"')
+ expect(hasMainBundleScript).toBe(false)
+
+ // The serialized router data script ($_TSR) should NOT be present
+ const hasRouterDataScript = html.includes('window.$_TSR')
+ expect(hasRouterDataScript).toBe(false)
+
+ // Verify that inline scripts from head() option are still present
+ const hasInlineScript = html.includes('External scripts still work')
+ expect(hasInlineScript).toBe(true)
+
+ // Verify that the page content is still rendered (SSR worked)
+ await expect(page.getByTestId('static-heading')).toContainText(
+ 'Static Route',
+ )
+ await expect(page.getByTestId('message')).toContainText(
+ 'This data was loaded on the server',
+ )
+
+ // The page should not be interactive (no hydration)
+ // Button with onClick should not work
+ await expect(page.getByTestId('inactive-button')).toBeVisible()
+ })
+
+ test('should render correct meta tags when hydrate is false', async ({
+ page,
+ }) => {
+ await page.goto('/static')
+
+ // Verify meta tags from head() option are present
+ const title = await page.title()
+ expect(title).toContain('Static Route')
+
+ const description = await page.getAttribute(
+ 'meta[name="description"]',
+ 'content',
+ )
+ expect(description).toBe('A static server-rendered page with no hydration')
+ })
+
+ test('should not include modulepreload links when hydrate is false', async ({
+ page,
+ }) => {
+ await page.goto('/static')
+
+ const html = await page.content()
+
+ // Modulepreload links should NOT be present
+ const hasModulePreload = html.includes('rel="modulepreload"')
+ expect(hasModulePreload).toBe(false)
+ })
+
+ test('should still serve static content correctly when hydrate is false', async ({
+ page,
+ }) => {
+ await page.goto('/static')
+
+ // Verify that loader data is rendered (SSR)
+ const message = await page.getByTestId('message').textContent()
+ expect(message).toContain('This data was loaded on the server')
+
+ // Verify server time is present (loader ran on server)
+ const serverTime = await page.getByTestId('server-time').textContent()
+ expect(serverTime).toBeTruthy()
+ expect(serverTime).toContain('Server Time:')
+
+ // Verify page views are rendered
+ const pageViews = await page.getByTestId('page-views').textContent()
+ expect(pageViews).toContain('Page Views:')
+ })
+
+ test('hydrated route should include all bundles and be interactive', async ({
+ page,
+ }) => {
+ // Visit the hydrated route for comparison
+ await page.goto('/hydrated')
+
+ // Wait for hydration to complete
+ await expect(page.getByTestId('hydration-status')).toContainText(
+ 'Hydrated and Interactive',
+ )
+
+ // Get the HTML content
+ const html = await page.content()
+
+ // The main bundle scripts SHOULD be present
+ const hasMainBundleScript = html.includes('type="module"')
+ expect(hasMainBundleScript).toBe(true)
+
+ // Verify interactivity works
+ const counter = page.getByTestId('counter')
+ await expect(counter).toContainText('0')
+
+ // Click the increment button
+ await page.click('button:has-text("+")')
+ await expect(counter).toContainText('1')
+
+ // Click the decrement button
+ await page.click('button:has-text("-")')
+ await expect(counter).toContainText('0')
+ })
+
+ test('navigation from home to static page should work', async ({ page }) => {
+ await page.goto('/')
+
+ // Click the static route link
+ await page.click('a[href="/static"]')
+
+ // Verify we're on the static page
+ await expect(page.getByTestId('static-heading')).toBeVisible()
+ await expect(page).toHaveURL('/static')
+ })
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/navigation.spec.ts b/e2e/react-start/basic-hydrate-false/tests/navigation.spec.ts
new file mode 100644
index 00000000000..ea5bc50a072
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/navigation.spec.ts
@@ -0,0 +1,77 @@
+import { expect } from '@playwright/test'
+
+import { test } from '@tanstack/router-e2e-utils'
+
+test.use({
+ whitelistErrors: [
+ /Failed to load resource: the server responded with a status of 404/,
+ ],
+})
+test('Navigating to post', async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await page.getByRole('link', { name: 'Deep View' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating to user', async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+ await page.getByRole('link', { name: 'Users' }).click()
+ await page.getByRole('link', { name: 'Leanne Graham' }).click()
+ await expect(page.getByRole('heading')).toContainText('Leanne Graham')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('body')).toContainText("I'm a layout")
+ await expect(page.locator('body')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('body')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('body')).toContainText("I'm layout B!")
+})
+
+test('client side navigating to a route with scripts', async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+ await page.getByRole('link', { name: 'Scripts', exact: true }).click()
+ await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+ expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+ expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined)
+})
+
+test('directly going to a route with scripts', async ({ page }) => {
+ await page.goto('/scripts')
+ await page.waitForURL('/scripts')
+ await page.waitForLoadState('networkidle')
+ expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+ expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined)
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
+
+test('Should change title on client side navigation', async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+
+ await expect(page).toHaveTitle('Posts page')
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/not-found.spec.ts b/e2e/react-start/basic-hydrate-false/tests/not-found.spec.ts
new file mode 100644
index 00000000000..0b5acc3b782
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/not-found.spec.ts
@@ -0,0 +1,77 @@
+import { expect } from '@playwright/test'
+import combinateImport from 'combinate'
+import { test } from '@tanstack/router-e2e-utils'
+import { isSpaMode } from '../tests/utils/isSpaMode'
+
+// somehow playwright does not correctly import default exports
+const combinate = (combinateImport as any).default as typeof combinateImport
+
+test.use({
+ whitelistErrors: [
+ /Failed to load resource: the server responded with a status of 404/,
+ 'NotFound error during hydration for routeId',
+ ],
+})
+test.describe('not-found', () => {
+ test(`global not found`, async ({ page }) => {
+ const response = await page.goto(`/this-page-does-not-exist/foo/bar`)
+
+ expect(response?.status()).toBe(isSpaMode ? 200 : 404)
+
+ await expect(
+ page.getByTestId('default-not-found-component'),
+ ).toBeInViewport()
+ })
+
+ test.describe('throw notFound()', () => {
+ const navigationTestMatrix = combinate({
+ // TODO beforeLoad!
+ thrower: [/* 'beforeLoad',*/ 'head', 'loader'] as const,
+ preload: [false, true] as const,
+ })
+
+ navigationTestMatrix.forEach(({ thrower, preload }) => {
+ test(`navigation: thrower: ${thrower}, preload: ${preload}`, async ({
+ page,
+ }) => {
+ await page.goto(
+ `/not-found/${preload === false ? '?preload=false' : ''}`,
+ )
+ const link = page.getByTestId(`via-${thrower}`)
+
+ if (preload) {
+ await link.focus()
+ await new Promise((r) => setTimeout(r, 250))
+ }
+
+ await link.click()
+
+ await expect(
+ page.getByTestId(`via-${thrower}-notFound-component`),
+ ).toBeInViewport()
+
+ await expect(
+ page.getByTestId(`via-${thrower}-route-component`),
+ ).not.toBeInViewport()
+ })
+ })
+ const directVisitTestMatrix = combinate({
+ // TODO beforeLoad!
+
+ thrower: [/* 'beforeLoad',*/ 'head', 'loader'] as const,
+ })
+
+ directVisitTestMatrix.forEach(({ thrower }) => {
+ test(`direct visit: thrower: ${thrower}`, async ({ page }) => {
+ await page.goto(`/not-found/via-${thrower}`)
+ await page.waitForLoadState('networkidle')
+ await expect(
+ page.getByTestId(`via-${thrower}-notFound-component`),
+ ).toBeInViewport()
+ await expect(
+ page.getByTestId(`via-${thrower}-route-component`),
+ ).not.toBeInViewport()
+ })
+ })
+ })
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/params.spec.ts b/e2e/react-start/basic-hydrate-false/tests/params.spec.ts
new file mode 100644
index 00000000000..737c82f35d2
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/params.spec.ts
@@ -0,0 +1,22 @@
+import { expect } from '@playwright/test'
+
+import { test } from '@tanstack/router-e2e-utils'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test.use({
+ whitelistErrors: [
+ /Failed to load resource: the server responded with a status of 404/,
+ ],
+})
+test.describe('Unicode route rendering', () => {
+ test('should render non-latin route correctly', async ({ page, baseURL }) => {
+ await page.goto('/대한민국')
+
+ await expect(page.locator('body')).toContainText('Hello "/대한민국"!')
+
+ expect(page.url()).toBe(`${baseURL}/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`)
+ })
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/prerendering.spec.ts b/e2e/react-start/basic-hydrate-false/tests/prerendering.spec.ts
new file mode 100644
index 00000000000..7718fe86ef0
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/prerendering.spec.ts
@@ -0,0 +1,43 @@
+import { existsSync, readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+import { isPrerender } from './utils/isPrerender'
+
+test.describe('Prerender Static Path Discovery', () => {
+ test.skip(!isPrerender, 'Skipping since not in prerender mode')
+ test.describe('Build Output Verification', () => {
+ test('should automatically discover and prerender static routes', () => {
+ // Check that static routes were automatically discovered and prerendered
+ const distDir = join(process.cwd(), 'dist', 'client')
+
+ // These static routes should be automatically discovered and prerendered
+ expect(existsSync(join(distDir, 'index.html'))).toBe(true)
+ expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)
+ expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
+ expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
+ expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
+ expect(existsSync(join(distDir, '대한민국/index.html'))).toBe(true)
+
+ // Pathless layouts should NOT be prerendered (they start with _)
+ expect(existsSync(join(distDir, '_layout', 'index.html'))).toBe(false) // /_layout
+
+ // API routes should NOT be prerendered
+
+ expect(existsSync(join(distDir, 'api', 'users', 'index.html'))).toBe(
+ false,
+ ) // /api/users
+ })
+ })
+
+ test.describe('Static Files Verification', () => {
+ test('should contain prerendered content in posts.html', () => {
+ const distDir = join(process.cwd(), 'dist', 'client')
+ expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)
+
+ // "Select a post." should be in the prerendered HTML
+ const html = readFileSync(join(distDir, 'posts/index.html'), 'utf-8')
+ expect(html).toContain('Select a post.')
+ })
+ })
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/redirect.spec.ts b/e2e/react-start/basic-hydrate-false/tests/redirect.spec.ts
new file mode 100644
index 00000000000..bc36de96230
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/redirect.spec.ts
@@ -0,0 +1,250 @@
+import queryString from 'node:querystring'
+import { expect } from '@playwright/test'
+import combinateImport from 'combinate'
+import {
+ getDummyServerPort,
+ getTestServerPort,
+ test,
+} from '@tanstack/router-e2e-utils'
+import { isSpaMode } from '../tests/utils/isSpaMode'
+import { isPreview } from '../tests/utils/isPreview'
+import packageJson from '../package.json' with { type: 'json' }
+
+// somehow playwright does not correctly import default exports
+const combinate = (combinateImport as any).default as typeof combinateImport
+
+const PORT = await getTestServerPort(
+ `${packageJson.name}${isSpaMode ? '_spa' : ''}${isPreview ? '_preview' : ''}`,
+)
+
+const EXTERNAL_HOST_PORT = await getDummyServerPort(packageJson.name)
+
+test.describe('redirects', () => {
+ test.describe('internal', () => {
+ const internalNavigationTestMatrix = combinate({
+ thrower: ['beforeLoad', 'loader'] as const,
+ reloadDocument: [false, true] as const,
+ preload: [false, true] as const,
+ })
+
+ internalNavigationTestMatrix.forEach(
+ ({ thrower, reloadDocument, preload }) => {
+ test(`internal target, navigation: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({
+ page,
+ }) => {
+ await page.goto(
+ `/redirect/internal${preload === false ? '?preload=false' : ''}`,
+ )
+
+ const link = page.getByTestId(
+ `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`,
+ )
+
+ await page.waitForLoadState('networkidle')
+ let requestHappened = false
+
+ const requestPromise = new Promise((resolve) => {
+ page.on('request', (request) => {
+ if (
+ request.url().startsWith(`http://localhost:${PORT}/_serverFn/`)
+ ) {
+ requestHappened = true
+ resolve()
+ }
+ })
+ })
+ await link.focus()
+
+ const expectRequestHappened = preload && !reloadDocument
+ const timeoutPromise = new Promise((resolve) =>
+ setTimeout(resolve, expectRequestHappened ? 5000 : 500),
+ )
+ await Promise.race([requestPromise, timeoutPromise])
+ expect(requestHappened).toBe(expectRequestHappened)
+ let fullPageLoad = false
+ page.on('domcontentloaded', () => {
+ fullPageLoad = true
+ })
+
+ await link.click()
+
+ const url = `http://localhost:${PORT}/posts`
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ expect(fullPageLoad).toBe(reloadDocument)
+ })
+ },
+ )
+
+ const internalDirectVisitTestMatrix = combinate({
+ thrower: ['beforeLoad', 'loader'] as const,
+ reloadDocument: [false, true] as const,
+ })
+
+ internalDirectVisitTestMatrix.forEach(({ thrower, reloadDocument }) => {
+ test(`internal target, direct visit: thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({
+ page,
+ }) => {
+ await page.goto(`/redirect/internal/via-${thrower}`)
+ await page.waitForLoadState('networkidle')
+
+ const url = `http://localhost:${PORT}/posts`
+
+ expect(page.url()).toBe(url)
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ })
+ })
+ })
+
+ test.describe('external', () => {
+ const externalTestMatrix = combinate({
+ scenario: ['navigate', 'direct_visit'] as const,
+ thrower: ['beforeLoad', 'loader'] as const,
+ })
+
+ externalTestMatrix.forEach(({ scenario, thrower }) => {
+ test(`external target: scenario: ${scenario}, thrower: ${thrower}`, async ({
+ page,
+ }) => {
+ const q = queryString.stringify({
+ externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`,
+ })
+
+ if (scenario === 'navigate') {
+ await page.goto(`/redirect/external?${q}`)
+ await page.waitForLoadState('networkidle')
+ const link = page.getByTestId(`via-${thrower}`)
+ await link.focus()
+ await link.click()
+ } else {
+ await page.goto(`/redirect/external/via-${thrower}?${q}`)
+ }
+
+ const url = `http://localhost:${EXTERNAL_HOST_PORT}/`
+
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ })
+ })
+ })
+
+ test.describe('serverFn', () => {
+ const serverFnTestMatrix = combinate({
+ target: ['internal', 'external'] as const,
+ scenario: ['navigate', 'direct_visit'] as const,
+ thrower: ['beforeLoad', 'loader'] as const,
+ reloadDocument: [false, true] as const,
+ })
+
+ serverFnTestMatrix.forEach(
+ ({ target, thrower, scenario, reloadDocument }) => {
+ test(`serverFn redirects to target: ${target}, scenario: ${scenario}, thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({
+ page,
+ }) => {
+ let fullPageLoad = false
+ const q = queryString.stringify({
+ externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`,
+ reloadDocument,
+ })
+
+ if (scenario === 'navigate') {
+ await page.goto(`/redirect/${target}/serverFn?${q}`)
+ await page.waitForLoadState('networkidle')
+
+ const link = page.getByTestId(
+ `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`,
+ )
+
+ page.on('domcontentloaded', () => {
+ fullPageLoad = true
+ })
+
+ await link.focus()
+ await page.waitForLoadState('networkidle')
+ await link.click()
+ } else {
+ await page.goto(`/redirect/${target}/serverFn/via-${thrower}?${q}`)
+ }
+
+ const url =
+ target === 'internal'
+ ? `http://localhost:${PORT}/posts`
+ : `http://localhost:${EXTERNAL_HOST_PORT}/`
+
+ await page.waitForURL(url)
+
+ expect(page.url()).toBe(url)
+
+ if (target === 'internal' && scenario === 'navigate') {
+ await expect(
+ page.getByTestId('PostsIndexComponent'),
+ ).toBeInViewport()
+ expect(fullPageLoad).toBe(reloadDocument)
+ }
+ })
+ },
+ )
+ })
+
+ test.describe('useServerFn', () => {
+ const useServerFnTestMatrix = combinate({
+ target: ['internal', 'external'] as const,
+ reloadDocument: [false, true] as const,
+ })
+
+ useServerFnTestMatrix.forEach(({ target, reloadDocument }) => {
+ test(`useServerFn redirects to target: ${target}, reloadDocument: ${reloadDocument}`, async ({
+ page,
+ }) => {
+ const q = queryString.stringify({
+ externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`,
+ reloadDocument,
+ })
+
+ await page.goto(`/redirect/${target}/serverFn/via-useServerFn?${q}`)
+
+ await page.waitForLoadState('networkidle')
+
+ const button = page.getByTestId('redirect-on-click')
+
+ let fullPageLoad = false
+ page.on('domcontentloaded', () => {
+ fullPageLoad = true
+ })
+
+ await button.click()
+
+ const url =
+ target === 'internal'
+ ? `http://localhost:${PORT}/posts`
+ : `http://localhost:${EXTERNAL_HOST_PORT}/`
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ if (target === 'internal') {
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ expect(fullPageLoad).toBe(reloadDocument)
+ }
+ })
+ })
+ })
+
+ test('multiple Set-Cookie headers are preserved on redirect', async ({
+ page,
+ }) => {
+ // This test verifies that multiple Set-Cookie headers are not lost during redirect
+ await page.goto('/multi-cookie-redirect')
+
+ // Wait for redirect to complete
+ await page.waitForURL(/\/multi-cookie-redirect\/target/)
+
+ // Should redirect to target page
+ await expect(page.getByTestId('multi-cookie-redirect-target')).toBeVisible()
+ expect(page.url()).toContain('/multi-cookie-redirect/target')
+
+ // Verify all three cookies were preserved during the redirect
+ await expect(page.getByTestId('cookie-session')).toHaveText('session-value')
+ await expect(page.getByTestId('cookie-csrf')).toHaveText('csrf-token-value')
+ await expect(page.getByTestId('cookie-theme')).toHaveText('dark')
+ })
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/script-duplication.spec.ts b/e2e/react-start/basic-hydrate-false/tests/script-duplication.spec.ts
new file mode 100644
index 00000000000..2ed98a3a73f
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/script-duplication.spec.ts
@@ -0,0 +1,143 @@
+import { expect, test } from '@playwright/test'
+
+test.describe('Script Duplication Prevention', () => {
+ test('should not create duplicate scripts on SSR route', async ({ page }) => {
+ await page.goto('/scripts')
+
+ await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+
+ const scriptCount = await page.evaluate(() => {
+ return document.querySelectorAll('script[src="script.js"]').length
+ })
+
+ expect(scriptCount).toBe(1)
+
+ expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+ })
+
+ test('should not create duplicate scripts during client-side navigation', async ({
+ page,
+ }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Scripts', exact: true }).click()
+ await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+
+ const firstNavCount = await page.evaluate(() => {
+ return document.querySelectorAll('script[src="script.js"]').length
+ })
+ expect(firstNavCount).toBe(1)
+
+ await page.getByRole('link', { name: 'Home' }).click()
+ await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible()
+
+ await page.getByRole('link', { name: 'Scripts', exact: true }).click()
+ await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+
+ const secondNavCount = await page.evaluate(() => {
+ return document.querySelectorAll('script[src="script.js"]').length
+ })
+ expect(secondNavCount).toBe(1)
+
+ expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+ })
+
+ test('should not create duplicate scripts with multiple navigation cycles', async ({
+ page,
+ }) => {
+ await page.goto('/')
+
+ for (let i = 0; i < 3; i++) {
+ await page.getByRole('link', { name: 'Scripts', exact: true }).click()
+ await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+
+ await page.getByRole('link', { name: 'Home' }).click()
+ await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible()
+ }
+
+ await page.getByRole('link', { name: 'Scripts', exact: true }).click()
+ await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+
+ const finalCount = await page.evaluate(() => {
+ return document.querySelectorAll('script[src="script.js"]').length
+ })
+ expect(finalCount).toBe(1)
+
+ expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+ })
+
+ test('should not create duplicate inline scripts', async ({ page }) => {
+ await page.goto('/inline-scripts')
+
+ await expect(
+ page.getByTestId('inline-scripts-test-heading'),
+ ).toBeInViewport()
+
+ const script1Count = await page.evaluate(() => {
+ const scripts = Array.from(document.querySelectorAll('script:not([src])'))
+ return scripts.filter(
+ (script) =>
+ script.textContent &&
+ script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
+ ).length
+ })
+
+ const script2Count = await page.evaluate(() => {
+ const scripts = Array.from(document.querySelectorAll('script:not([src])'))
+ return scripts.filter(
+ (script) =>
+ script.textContent &&
+ script.textContent.includes('window.INLINE_SCRIPT_2 = "test"'),
+ ).length
+ })
+
+ expect(script1Count).toBe(1)
+ expect(script2Count).toBe(1)
+
+ expect(await page.evaluate('window.INLINE_SCRIPT_1')).toBe(true)
+ expect(await page.evaluate('window.INLINE_SCRIPT_2')).toBe('test')
+ })
+
+ test('should not create duplicate inline scripts during client-side navigation', async ({
+ page,
+ }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Inline Scripts' }).click()
+ await expect(
+ page.getByTestId('inline-scripts-test-heading'),
+ ).toBeInViewport()
+
+ const firstNavScript1Count = await page.evaluate(() => {
+ const scripts = Array.from(document.querySelectorAll('script:not([src])'))
+ return scripts.filter(
+ (script) =>
+ script.textContent &&
+ script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
+ ).length
+ })
+ expect(firstNavScript1Count).toBe(1)
+
+ await page.getByRole('link', { name: 'Home' }).click()
+ await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible()
+
+ await page.getByRole('link', { name: 'Inline Scripts' }).click()
+ await expect(
+ page.getByTestId('inline-scripts-test-heading'),
+ ).toBeInViewport()
+
+ const secondNavScript1Count = await page.evaluate(() => {
+ const scripts = Array.from(document.querySelectorAll('script:not([src])'))
+ return scripts.filter(
+ (script) =>
+ script.textContent &&
+ script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
+ ).length
+ })
+ expect(secondNavScript1Count).toBe(1)
+
+ // Verify the scripts are still working
+ expect(await page.evaluate('window.INLINE_SCRIPT_1')).toBe(true)
+ expect(await page.evaluate('window.INLINE_SCRIPT_2')).toBe('test')
+ })
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/search-params.spec.ts b/e2e/react-start/basic-hydrate-false/tests/search-params.spec.ts
new file mode 100644
index 00000000000..74c1aa54dc9
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/search-params.spec.ts
@@ -0,0 +1,100 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+import { isSpaMode } from 'tests/utils/isSpaMode'
+import { isPrerender } from './utils/isPrerender'
+import type { Response } from '@playwright/test'
+
+function expectRedirect(response: Response | null, endsWith: string) {
+ expect(response).not.toBeNull()
+ expect(response!.request().redirectedFrom()).not.toBeNull()
+ const redirectUrl = response!
+ .request()
+ .redirectedFrom()!
+ .redirectedTo()
+ ?.url()
+ expect(redirectUrl).toBeDefined()
+ expect(redirectUrl!.endsWith(endsWith))
+}
+
+function expectNoRedirect(response: Response | null) {
+ expect(response).not.toBeNull()
+ const request = response!.request()
+ expect(request.redirectedFrom()).toBeNull()
+}
+
+test.describe('/search-params/loader-throws-redirect', () => {
+ test('Directly visiting the route without search param set', async ({
+ page,
+ }) => {
+ const response = await page.goto('/search-params/loader-throws-redirect')
+
+ if (!isSpaMode && !isPrerender) {
+ expectRedirect(response, '/search-params/loader-throws-redirect?step=a')
+ }
+
+ await expect(page.getByTestId('search-param')).toContainText('a')
+ expect(page.url().endsWith('/search-params/loader-throws-redirect?step=a'))
+ })
+
+ test('Directly visiting the route with search param set', async ({
+ page,
+ }) => {
+ const response = await page.goto(
+ '/search-params/loader-throws-redirect?step=b',
+ )
+ expectNoRedirect(response)
+ await expect(page.getByTestId('search-param')).toContainText('b')
+ expect(page.url().endsWith('/search-params/loader-throws-redirect?step=b'))
+ })
+})
+
+test.describe('/search-params/default', () => {
+ test('Directly visiting the route without search param set', async ({
+ page,
+ }) => {
+ const response = await page.goto('/search-params/default')
+ if (!isSpaMode && !isPrerender) {
+ expectRedirect(response, '/search-params/default?default=d1')
+ }
+ await expect(page.getByTestId('search-default')).toContainText('d1')
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+ expect(
+ page.url().endsWith('/search-params/default?default=d1'),
+ ).toBeTruthy()
+ })
+
+ test('Directly visiting the route with search param set', async ({
+ page,
+ }) => {
+ const response = await page.goto('/search-params/default?default=d2')
+ expectNoRedirect(response)
+
+ await expect(page.getByTestId('search-default')).toContainText('d2')
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+ expect(
+ page.url().endsWith('/search-params/default?default=d2'),
+ ).toBeTruthy()
+ })
+
+ test('navigating to the route without search param set', async ({ page }) => {
+ await page.goto('/search-params/')
+ await page.getByTestId('link-to-default-without-search').click()
+
+ await expect(page.getByTestId('search-default')).toContainText('d1')
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+ expect(
+ page.url().endsWith('/search-params/default?default=d1'),
+ ).toBeTruthy()
+ })
+
+ test('navigating to the route with search param set', async ({ page }) => {
+ await page.goto('/search-params/')
+ await page.getByTestId('link-to-default-with-search').click()
+
+ await expect(page.getByTestId('search-default')).toContainText('d2')
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+ expect(
+ page.url().endsWith('/search-params/default?default=d2'),
+ ).toBeTruthy()
+ })
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/setup/global.setup.ts b/e2e/react-start/basic-hydrate-false/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+ await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/basic-hydrate-false/tests/setup/global.teardown.ts b/e2e/react-start/basic-hydrate-false/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+ await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/basic-hydrate-false/tests/streaming.spec.ts b/e2e/react-start/basic-hydrate-false/tests/streaming.spec.ts
new file mode 100644
index 00000000000..15f60b7deb9
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/streaming.spec.ts
@@ -0,0 +1,35 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test('Navigating to deferred route', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Deferred' }).click()
+
+ await expect(page.getByTestId('regular-person')).toContainText('John Doe')
+ await expect(page.getByTestId('deferred-person')).toContainText(
+ 'Tanner Linsley',
+ )
+ await expect(page.getByTestId('deferred-stuff')).toContainText(
+ 'Hello deferred!',
+ )
+})
+
+test('Directly visiting the deferred route', async ({ page }) => {
+ await page.goto('/deferred')
+
+ await expect(page.getByTestId('regular-person')).toContainText('John Doe')
+ await expect(page.getByTestId('deferred-person')).toContainText(
+ 'Tanner Linsley',
+ )
+ await expect(page.getByTestId('deferred-stuff')).toContainText(
+ 'Hello deferred!',
+ )
+})
+
+test('streaming loader data', async ({ page }) => {
+ await page.goto('/stream')
+
+ await expect(page.getByTestId('promise-data')).toContainText('promise-data')
+ await expect(page.getByTestId('stream-data')).toContainText('stream-data')
+})
diff --git a/e2e/react-start/basic-hydrate-false/tests/utils/isPrerender.ts b/e2e/react-start/basic-hydrate-false/tests/utils/isPrerender.ts
new file mode 100644
index 00000000000..d5d991d4545
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/utils/isPrerender.ts
@@ -0,0 +1 @@
+export const isPrerender: boolean = process.env.MODE === 'prerender'
diff --git a/e2e/react-start/basic-hydrate-false/tests/utils/isPreview.ts b/e2e/react-start/basic-hydrate-false/tests/utils/isPreview.ts
new file mode 100644
index 00000000000..7ea362a83ed
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/utils/isPreview.ts
@@ -0,0 +1 @@
+export const isPreview: boolean = process.env.MODE === 'preview'
diff --git a/e2e/react-start/basic-hydrate-false/tests/utils/isSpaMode.ts b/e2e/react-start/basic-hydrate-false/tests/utils/isSpaMode.ts
new file mode 100644
index 00000000000..b4edb829a8f
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tests/utils/isSpaMode.ts
@@ -0,0 +1 @@
+export const isSpaMode: boolean = process.env.MODE === 'spa'
diff --git a/e2e/react-start/basic-hydrate-false/tsconfig.json b/e2e/react-start/basic-hydrate-false/tsconfig.json
new file mode 100644
index 00000000000..b3a2d67dfa6
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["**/*.ts", "**/*.tsx", "public/script*.js"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "target": "ES2022",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./src/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/e2e/react-start/basic-hydrate-false/vite.config.ts b/e2e/react-start/basic-hydrate-false/vite.config.ts
new file mode 100644
index 00000000000..ecab96f2d8b
--- /dev/null
+++ b/e2e/react-start/basic-hydrate-false/vite.config.ts
@@ -0,0 +1,44 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+import { isSpaMode } from './tests/utils/isSpaMode'
+import { isPrerender } from './tests/utils/isPrerender'
+
+const spaModeConfiguration = {
+ enabled: true,
+ prerender: {
+ outputPath: 'index.html',
+ },
+}
+
+const prerenderConfiguration = {
+ enabled: true,
+ filter: (page: { path: string }) =>
+ ![
+ '/this-route-does-not-exist',
+ '/redirect',
+ '/i-do-not-exist',
+ '/not-found/via-beforeLoad',
+ '/not-found/via-head',
+ '/not-found/via-loader',
+ '/users',
+ ].some((p) => page.path.includes(p)),
+ maxRedirects: 100,
+}
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ spa: isSpaMode ? spaModeConfiguration : undefined,
+ prerender: isPrerender ? prerenderConfiguration : undefined,
+ }),
+ viteReact(),
+ ],
+})
diff --git a/examples/react/start-basic/src/routes/__root.tsx b/examples/react/start-basic/src/routes/__root.tsx
index 346409e9d91..8e9d52658e6 100644
--- a/examples/react/start-basic/src/routes/__root.tsx
+++ b/examples/react/start-basic/src/routes/__root.tsx
@@ -13,6 +13,7 @@ import appCss from '~/styles/app.css?url'
import { seo } from '~/utils/seo'
export const Route = createRootRoute({
+ hydrate: false,
head: () => ({
meta: [
{
diff --git a/packages/react-router/src/HeadContent.tsx b/packages/react-router/src/HeadContent.tsx
index 617498c8572..ba4bfeb0cab 100644
--- a/packages/react-router/src/HeadContent.tsx
+++ b/packages/react-router/src/HeadContent.tsx
@@ -2,6 +2,7 @@ import * as React from 'react'
import { Asset } from './Asset'
import { useRouter } from './useRouter'
import { useRouterState } from './useRouterState'
+import { getHydrateStatus } from './hydrate-status'
import type { RouterManagedTag } from '@tanstack/router-core'
/**
@@ -115,6 +116,13 @@ export const useTags = () => {
const preloadLinks = useRouterState({
select: (state) => {
+ const { shouldHydrate } = getHydrateStatus(state.matches, router)
+
+ // If hydrate is false, don't include modulepreload links
+ if (!shouldHydrate) {
+ return []
+ }
+
const preloadLinks: Array = []
state.matches
diff --git a/packages/react-router/src/Scripts.tsx b/packages/react-router/src/Scripts.tsx
index 3765e5790d8..3b9da8ec11f 100644
--- a/packages/react-router/src/Scripts.tsx
+++ b/packages/react-router/src/Scripts.tsx
@@ -1,6 +1,7 @@
import { Asset } from './Asset'
import { useRouterState } from './useRouterState'
import { useRouter } from './useRouter'
+import { getHydrateStatus } from './hydrate-status'
import type { RouterManagedTag } from '@tanstack/router-core'
/**
@@ -14,8 +15,14 @@ import type { RouterManagedTag } from '@tanstack/router-core'
export const Scripts = () => {
const router = useRouter()
const nonce = router.options.ssr?.nonce
+
+ const hydrateStatus = useRouterState({
+ select: (state) => getHydrateStatus(state.matches, router),
+ })
+
const assetScripts = useRouterState({
select: (state) => {
+ const { shouldHydrate } = getHydrateStatus(state.matches, router)
const assetScripts: Array = []
const manifest = router.ssr?.manifest
@@ -29,6 +36,25 @@ export const Scripts = () => {
manifest.routes[route.id]?.assets
?.filter((d) => d.tag === 'script')
.forEach((asset) => {
+ // If hydrate is false, skip client entry scripts
+ if (!shouldHydrate) {
+ // Check by attribute
+ if (
+ asset.attrs?.['data-tsr-client-entry'] === 'true' ||
+ asset.attrs?.['data-tsr-client-entry'] === true
+ ) {
+ return
+ }
+
+ // Also check by content as fallback
+ const scriptContent = asset.children?.toString() || ''
+ if (
+ scriptContent.includes('virtual:tanstack-start-client-entry')
+ ) {
+ return
+ }
+ }
+
assetScripts.push({
tag: 'script',
attrs: { ...asset.attrs, nonce },
@@ -42,9 +68,11 @@ export const Scripts = () => {
structuralSharing: true as any,
})
- const { scripts } = useRouterState({
- select: (state) => ({
- scripts: (
+ const scripts = useRouterState({
+ select: (state) => {
+ const { shouldHydrate } = getHydrateStatus(state.matches, router)
+
+ const allScripts = (
state.matches
.map((match) => match.scripts!)
.flat(1)
@@ -57,14 +85,62 @@ export const Scripts = () => {
nonce,
},
children,
- })),
- }),
+ }))
+
+ // If hydrate is false, remove client entry imports but keep React Refresh for HMR
+ if (!shouldHydrate) {
+ return allScripts
+ .map((script) => {
+ const scriptContent = script.children?.toString() || ''
+
+ // If this script contains the client entry import, remove that import
+ // but keep the React Refresh setup for development HMR
+ if (scriptContent.includes('virtual:tanstack-start-client-entry')) {
+ // Remove the import line(s) that load the client entry
+ const modifiedContent = scriptContent
+ .split('\n')
+ .filter(
+ (line) =>
+ !line.includes('virtual:tanstack-start-client-entry'),
+ )
+ .join('\n')
+ .trim()
+
+ // If there's still content (React Refresh setup), keep it
+ if (modifiedContent) {
+ return {
+ ...script,
+ children: modifiedContent,
+ }
+ }
+ // If filtering removed everything, exclude this script
+ return null
+ }
+
+ return script
+ })
+ .filter(Boolean) as Array
+ }
+
+ return allScripts
+ },
structuralSharing: true as any,
})
+ // Warn about conflicting hydrate options
+ if (hydrateStatus.hasConflict) {
+ console.warn(
+ '⚠️ [TanStack Router] Conflicting hydrate options detected in route matches.\n' +
+ 'Some routes have hydrate: false while others have hydrate: true.\n' +
+ 'The page will be hydrated, but this may not be the intended behavior.\n' +
+ 'Please ensure all routes in the match have consistent hydrate settings.',
+ )
+ }
+
let serverBufferedScript: RouterManagedTag | undefined = undefined
- if (router.serverSsr) {
+ // Only include server buffered script if we're hydrating
+ if (router.serverSsr && hydrateStatus.shouldHydrate) {
serverBufferedScript = router.serverSsr.takeBufferedScripts()
}
diff --git a/packages/react-router/src/hydrate-status.ts b/packages/react-router/src/hydrate-status.ts
new file mode 100644
index 00000000000..6f814bd5670
--- /dev/null
+++ b/packages/react-router/src/hydrate-status.ts
@@ -0,0 +1,30 @@
+export function getHydrateStatus(
+ matches: Array,
+ router: any,
+): {
+ shouldHydrate: boolean
+ hasConflict: boolean
+} {
+ let hasExplicitFalse = false
+ let hasExplicitTrue = false
+ const defaultHydrate = router.options.defaultHydrate ?? true
+
+ matches.forEach((match) => {
+ const route = router.looseRoutesById[match.routeId]
+ const hydrateOption = route?.options.hydrate ?? defaultHydrate
+
+ if (hydrateOption === false) {
+ hasExplicitFalse = true
+ } else if (hydrateOption === true && route?.options.hydrate !== undefined) {
+ // Only count as explicit true if it was actually set on the route
+ hasExplicitTrue = true
+ }
+ })
+
+ const hasConflict = hasExplicitFalse && hasExplicitTrue
+
+ // If any route has false, don't hydrate (even if there's a conflict)
+ const shouldHydrate = !hasExplicitFalse
+
+ return { shouldHydrate, hasConflict }
+}
diff --git a/packages/router-core/src/Matches.ts b/packages/router-core/src/Matches.ts
index 852d186b67d..9ba0d4771d7 100644
--- a/packages/router-core/src/Matches.ts
+++ b/packages/router-core/src/Matches.ts
@@ -8,7 +8,12 @@ import type {
RouteById,
RouteIds,
} from './routeInfo'
-import type { AnyRouter, RegisteredRouter, SSROption } from './router'
+import type {
+ AnyRouter,
+ HydrateOption,
+ RegisteredRouter,
+ SSROption,
+} from './router'
import type { Constrain, ControlledPromise } from './utils'
export type AnyMatchAndValue = { match: any; value: any }
@@ -170,6 +175,8 @@ export interface RouteMatch<
staticData: StaticDataRouteOption
/** This attribute is not reactive */
ssr?: SSROption
+ /** This attribute is not reactive */
+ hydrate?: HydrateOption
_forcePending?: boolean
_displayPending?: boolean
}
@@ -193,6 +200,7 @@ export interface PreValidationErrorHandlingRouteMatch<
| { status: 'error'; error: unknown }
staticData: StaticDataRouteOption
ssr?: boolean | 'data-only'
+ hydrate?: boolean
}
export type MakePreValidationErrorHandlingRouteMatchUnion<
diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts
index 53d726ef05a..6e9dd90854a 100644
--- a/packages/router-core/src/route.ts
+++ b/packages/router-core/src/route.ts
@@ -15,7 +15,13 @@ import type {
} from './Matches'
import type { RootRouteId } from './root'
import type { ParseRoute, RouteById, RouteIds, RoutePaths } from './routeInfo'
-import type { AnyRouter, Register, RegisteredRouter, SSROption } from './router'
+import type {
+ AnyRouter,
+ HydrateOption,
+ Register,
+ RegisteredRouter,
+ SSROption,
+} from './router'
import type { BuildLocationFn, NavigateFn } from './RouterProvider'
import type {
Assign,
@@ -953,6 +959,8 @@ export interface FilebaseRouteOptionsInterface<
) => Awaitable)
>
+ hydrate?: undefined | HydrateOption
+
// This async function is called before a route is loaded.
// If an error is thrown here, the route's loader will not be called.
// If thrown during a navigation, the navigation will be cancelled and the error will be passed to the `onError` function.
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 19b77531297..925b08796c2 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -142,6 +142,8 @@ export interface RouterOptionsExtensions
export type SSROption = boolean | 'data-only'
+export type HydrateOption = boolean
+
export interface RouterOptions<
TRouteTree extends AnyRoute,
TTrailingSlashOption extends TrailingSlashOption,
@@ -393,6 +395,13 @@ export interface RouterOptions<
*/
defaultSsr?: SSROption
+ /**
+ * The default `hydrate` a route should use if no `hydrate` is provided.
+ *
+ * @default true
+ */
+ defaultHydrate?: HydrateOption
+
search?: {
/**
* Configures how unknown search params (= not returned by any `validateSearch`) are treated.
@@ -617,7 +626,7 @@ export type RouterConstructorOptions<
TRouterHistory,
TDehydrated
>,
- 'context' | 'serializationAdapters' | 'defaultSsr'
+ 'context' | 'serializationAdapters' | 'defaultSsr' | 'defaultHydrate'
> &
RouterContextOptions
diff --git a/packages/start-server-core/src/router-manifest.ts b/packages/start-server-core/src/router-manifest.ts
index 8e6f268f977..317c859b5bf 100644
--- a/packages/start-server-core/src/router-manifest.ts
+++ b/packages/start-server-core/src/router-manifest.ts
@@ -16,22 +16,33 @@ export async function getStartManifest() {
rootRoute.assets = rootRoute.assets || []
- let script = `import('${startManifest.clientEntry}')`
+ // In dev mode, add React Refresh separately so it can be kept for hydrate: false
if (process.env.TSS_DEV_SERVER === 'true') {
const { injectedHeadScripts } = await import(
'tanstack-start-injected-head-scripts:v'
)
if (injectedHeadScripts) {
- script = `${injectedHeadScripts + ';'}${script}`
+ // Add React Refresh script (keep for HMR even when hydrate: false)
+ rootRoute.assets.push({
+ tag: 'script',
+ attrs: {
+ type: 'module',
+ async: true,
+ },
+ children: injectedHeadScripts,
+ })
}
}
+
+ // Add client entry (will be filtered when hydrate: false)
rootRoute.assets.push({
tag: 'script',
attrs: {
type: 'module',
async: true,
+ 'data-tsr-client-entry': 'true',
},
- children: script,
+ children: `import('${startManifest.clientEntry}')`,
})
const manifest = {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 35c2e6f9a20..ad920353d5b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1259,6 +1259,88 @@ importers:
specifier: ^4.49.1
version: 4.49.1
+ e2e/react-start/basic-hydrate-false:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/react-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/react-router-devtools
+ '@tanstack/react-start':
+ specifier: workspace:*
+ version: link:../../../packages/react-start
+ express:
+ specifier: ^5.1.0
+ version: 5.1.0
+ http-proxy-middleware:
+ specifier: ^3.0.5
+ version: 3.0.5
+ js-cookie:
+ specifier: ^3.0.5
+ version: 3.0.5
+ react:
+ specifier: ^19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.0(react@19.2.0)
+ redaxios:
+ specifier: ^0.5.1
+ version: 0.5.1
+ tailwind-merge:
+ specifier: ^2.6.0
+ version: 2.6.0
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tailwindcss/postcss':
+ specifier: ^4.1.15
+ version: 4.1.15
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/js-cookie':
+ specifier: ^3.0.6
+ version: 3.0.6
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ '@types/react':
+ specifier: ^19.2.2
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.2(@types/react@19.2.2)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ combinate:
+ specifier: ^1.1.11
+ version: 1.1.11
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ srvx:
+ specifier: ^0.8.6
+ version: 0.8.15
+ tailwindcss:
+ specifier: ^4.1.17
+ version: 4.1.17
+ typescript:
+ specifier: ^5.7.2
+ version: 5.9.2
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vite-tsconfig-paths:
+ specifier: ^5.1.4
+ version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ zod:
+ specifier: ^3.24.2
+ version: 3.25.57
+
e2e/react-start/basic-react-query:
dependencies:
'@tanstack/react-query':