Skip to content

Commit d728d69

Browse files
feat: ssr setting per route (#4565)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 7938ff4 commit d728d69

File tree

229 files changed

+3572
-460
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

229 files changed

+3572
-460
lines changed

docs/router/eslint/create-route-property-order.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ For the following functions, the property order of the passed in object matters
1313
The correct property order is as follows
1414

1515
- `params`, `validateSearch`
16-
- `loaderDeps`, `search.middlewares`
16+
- `loaderDeps`, `search.middlewares`, `ssr`
1717
- `context`
1818
- `beforeLoad`
1919
- `loader`

docs/start/config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
"label": "Server Routes",
5050
"to": "framework/react/server-routes"
5151
},
52+
{
53+
"label": "Selective SSR",
54+
"to": "framework/react/selective-ssr"
55+
},
5256
{
5357
"label": "SPA Mode",
5458
"to": "framework/react/spa-mode"
@@ -126,6 +130,10 @@
126130
"label": "Server Routes",
127131
"to": "framework/solid/server-routes"
128132
},
133+
{
134+
"label": "Selective SSR",
135+
"to": "framework/solid/selective-ssr"
136+
},
129137
{
130138
"label": "SPA Mode",
131139
"to": "framework/solid/spa-mode"
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
---
2+
id: selective-ssr
3+
title: Selective Server-Side Rendering (SSR)
4+
---
5+
6+
## What is Selective SSR?
7+
8+
In TanStack Start, routes matching the initial request are rendered on the server by default. This means `beforeLoad` and `loader` are executed on the server, followed by rendering the route components. The resulting HTML is sent to the client, which hydrates the markup into a fully interactive application.
9+
10+
However, there are cases where you might want to disable SSR for certain routes or all routes, such as:
11+
12+
- When `beforeLoad` or `loader` requires browser-only APIs (e.g., `localStorage`).
13+
- When the route component depends on browser-only APIs (e.g., `canvas`).
14+
15+
TanStack Start's Selective SSR feature lets you configure:
16+
17+
- Which routes should execute `beforeLoad` or `loader` on the server.
18+
- Which route components should be rendered on the server.
19+
20+
## How does this compare to SPA mode?
21+
22+
TanStack Start's [SPA mode](../spa-mode) completely disables server-side execution of `beforeLoad` and `loader`, as well as server-side rendering of route components. Selective SSR allows you to configure server-side handling on a per-route basis, either statically or dynamically.
23+
24+
## Configuration
25+
26+
You can control how a route is handled during the initial server request using the `ssr` property. If this property is not set, it defaults to `true`. You can change this default using the `defaultSsr` option in `createRouter`:
27+
28+
```tsx
29+
// src/router.tsx
30+
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
31+
import { routeTree } from './routeTree.gen'
32+
33+
export function createRouter() {
34+
const router = createTanStackRouter({
35+
routeTree,
36+
scrollRestoration: true,
37+
defaultPendingComponent: () => <div>Loading...</div>,
38+
// Disable SSR by default
39+
defaultSsr: false,
40+
})
41+
42+
return router
43+
}
44+
```
45+
46+
### `ssr: true`
47+
48+
This is the default behavior unless otherwise configured. On the initial request, it will:
49+
50+
- Run `beforeLoad` on the server and send the resulting context to the client.
51+
- Run `loader` on the server and send the loader data to the client.
52+
- Render the component on the server and send the HTML markup to the client.
53+
54+
```tsx
55+
// src/routes/posts/$postId.tsx
56+
export const Route = createFileRoute('/posts/$postId')({
57+
ssr: true,
58+
beforeLoad: () => {
59+
console.log('Executes on the server during the initial request')
60+
console.log('Executes on the client for subsequent navigation')
61+
},
62+
loader: () => {
63+
console.log('Executes on the server during the initial request')
64+
console.log('Executes on the client for subsequent navigation')
65+
},
66+
component: () => <div>This component is rendered on the client</div>,
67+
})
68+
```
69+
70+
### `ssr: false`
71+
72+
This disables server-side:
73+
74+
- Execution of the route's `beforeLoad` and `loader`.
75+
- Rendering of the route component.
76+
77+
```tsx
78+
// src/routes/posts/$postId.tsx
79+
export const Route = createFileRoute('/posts/$postId')({
80+
ssr: false,
81+
beforeLoad: () => {
82+
console.log('Executes on the client during hydration')
83+
},
84+
loader: () => {
85+
console.log('Executes on the client during hydration')
86+
},
87+
component: () => <div>This component is rendered on the client</div>,
88+
})
89+
```
90+
91+
### `ssr: 'data-only'`
92+
93+
This hybrid option will:
94+
95+
- Run `beforeLoad` on the server and send the resulting context to the client.
96+
- Run `loader` on the server and send the loader data to the client.
97+
- Disable server-side rendering of the route component.
98+
99+
```tsx
100+
// src/routes/posts/$postId.tsx
101+
export const Route = createFileRoute('/posts/$postId')({
102+
ssr: 'data-only',
103+
beforeLoad: () => {
104+
console.log('Executes on the server during the initial request')
105+
console.log('Executes on the client for subsequent navigation')
106+
},
107+
loader: () => {
108+
console.log('Executes on the server during the initial request')
109+
console.log('Executes on the client for subsequent navigation')
110+
},
111+
component: () => <div>This component is rendered on the client</div>,
112+
})
113+
```
114+
115+
### Functional Form
116+
117+
For more flexibility, you can use the functional form of the `ssr` property to decide at runtime whether to SSR a route:
118+
119+
```tsx
120+
// src/routes/docs/$docType/$docId.tsx
121+
export const Route = createFileRoute('/docs/$docType/$docId')({
122+
validateSearch: z.object({ details: z.boolean().optional() }),
123+
ssr: ({ params, search }) => {
124+
if (params.status === 'success' && params.value.docType === 'sheet') {
125+
return false
126+
}
127+
if (search.status === 'success' && search.value.details) {
128+
return 'data-only'
129+
}
130+
},
131+
beforeLoad: () => {
132+
console.log('Executes on the server depending on the result of ssr()')
133+
},
134+
loader: () => {
135+
console.log('Executes on the server depending on the result of ssr()')
136+
},
137+
component: () => <div>This component is rendered on the client</div>,
138+
})
139+
```
140+
141+
The `ssr` function runs only on the server during the initial request and is stripped from the client bundle.
142+
143+
`search` and `params` are passed in after validation as a discriminated union:
144+
145+
```tsx
146+
params:
147+
| { status: 'success'; value: Expand<ResolveAllParamsFromParent<TParentRoute, TParams>> }
148+
| { status: 'error'; error: unknown }
149+
search:
150+
| { status: 'success'; value: Expand<ResolveFullSearchSchema<TParentRoute, TSearchValidator>> }
151+
| { status: 'error'; error: unknown }
152+
```
153+
154+
If validation fails, `status` will be `error` and `error` will contain the failure details. Otherwise, `status` will be `success` and `value` will contain the validated data.
155+
156+
### Inheritance
157+
158+
At runtime, a child route inherits the Selective SSR configuration of its parent. For example:
159+
160+
```tsx
161+
root { ssr: undefined }
162+
posts { ssr: false }
163+
$postId { ssr: true }
164+
```
165+
166+
- `root` defaults to `ssr: true`.
167+
- `posts` explicitly sets `ssr: false`, so neither `beforeLoad` nor `loader` will run on the server, and the route component won't be rendered on the server.
168+
- `$postId` sets `ssr: true`, but inherits `ssr: false` from its parent.
169+
170+
Another example:
171+
172+
```tsx
173+
root { ssr: undefined }
174+
posts { ssr: 'data-only' }
175+
$postId { ssr: true }
176+
details { ssr: false }
177+
```
178+
179+
- `root` defaults to `ssr: true`.
180+
- `posts` sets `ssr: 'data-only'`, so `beforeLoad` and `loader` run on the server, but the route component isn't rendered on the server.
181+
- `$postId` sets `ssr: true`, but inherits `ssr: 'data-only'` from its parent.
182+
- `details` sets `ssr: false`, so neither `beforeLoad` nor `loader` will run on the server, and the route component won't be rendered on the server.
183+
184+
## Fallback Rendering
185+
186+
For the first route with `ssr: false` or `ssr: 'data-only'`, the server will render the route's `pendingComponent` as a fallback. If `pendingComponent` isn't configured, the `defaultPendingComponent` will be rendered. If neither is configured, no fallback will be rendered.
187+
188+
On the client during hydration, this fallback will be displayed for at least `minPendingMs` (or `defaultPendingMinMs` if not configured), even if the route doesn't have `beforeLoad` or `loader` defined.

0 commit comments

Comments
 (0)