@tanstack/react-start 1.166.14 → 1.166.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/intent.js +25 -0
- package/package.json +17 -8
- package/skills/lifecycle/migrate-from-nextjs/SKILL.md +435 -0
- package/skills/react-start/SKILL.md +285 -0
package/bin/intent.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Auto-generated by @tanstack/intent setup
|
|
3
|
+
// Exposes the intent end-user CLI for consumers of this library.
|
|
4
|
+
// Commit this file, then add to your package.json:
|
|
5
|
+
// "bin": { "intent": "./bin/intent.js" }
|
|
6
|
+
try {
|
|
7
|
+
await import('@tanstack/intent/intent-library')
|
|
8
|
+
} catch (e) {
|
|
9
|
+
const isModuleNotFound =
|
|
10
|
+
e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND'
|
|
11
|
+
const missingIntentLibrary =
|
|
12
|
+
typeof e?.message === 'string' && e.message.includes('@tanstack/intent')
|
|
13
|
+
|
|
14
|
+
if (isModuleNotFound && missingIntentLibrary) {
|
|
15
|
+
console.error('@tanstack/intent is not installed.')
|
|
16
|
+
console.error('')
|
|
17
|
+
console.error('Install it as a dev dependency:')
|
|
18
|
+
console.error(' npm add -D @tanstack/intent')
|
|
19
|
+
console.error('')
|
|
20
|
+
console.error('Or run directly:')
|
|
21
|
+
console.error(' npx @tanstack/intent@latest list')
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
throw e
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-start",
|
|
3
|
-
"version": "1.166.
|
|
3
|
+
"version": "1.166.16",
|
|
4
4
|
"description": "Modern and scalable routing for React applications",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -91,26 +91,35 @@
|
|
|
91
91
|
"sideEffects": false,
|
|
92
92
|
"files": [
|
|
93
93
|
"dist",
|
|
94
|
-
"src"
|
|
94
|
+
"src",
|
|
95
|
+
"skills",
|
|
96
|
+
"bin",
|
|
97
|
+
"!skills/_artifacts"
|
|
95
98
|
],
|
|
96
99
|
"engines": {
|
|
97
100
|
"node": ">=22.12.0"
|
|
98
101
|
},
|
|
99
102
|
"dependencies": {
|
|
100
103
|
"pathe": "^2.0.3",
|
|
101
|
-
"@tanstack/react-start-client": "1.166.
|
|
102
|
-
"@tanstack/react-start-server": "1.166.
|
|
104
|
+
"@tanstack/react-start-client": "1.166.13",
|
|
105
|
+
"@tanstack/react-start-server": "1.166.13",
|
|
103
106
|
"@tanstack/router-utils": "^1.161.6",
|
|
104
|
-
"@tanstack/start-plugin-core": "1.
|
|
105
|
-
"@tanstack/react-router": "1.167.
|
|
106
|
-
"@tanstack/start-
|
|
107
|
-
"@tanstack/start-
|
|
107
|
+
"@tanstack/start-plugin-core": "1.167.0",
|
|
108
|
+
"@tanstack/react-router": "1.167.4",
|
|
109
|
+
"@tanstack/start-server-core": "1.166.12",
|
|
110
|
+
"@tanstack/start-client-core": "1.166.12"
|
|
108
111
|
},
|
|
109
112
|
"peerDependencies": {
|
|
110
113
|
"react": ">=18.0.0 || >=19.0.0",
|
|
111
114
|
"react-dom": ">=18.0.0 || >=19.0.0",
|
|
112
115
|
"vite": ">=7.0.0"
|
|
113
116
|
},
|
|
117
|
+
"devDependencies": {
|
|
118
|
+
"@tanstack/intent": "^0.0.14"
|
|
119
|
+
},
|
|
120
|
+
"bin": {
|
|
121
|
+
"intent": "./bin/intent.js"
|
|
122
|
+
},
|
|
114
123
|
"scripts": {
|
|
115
124
|
"clean": "rimraf ./dist && rimraf ./coverage",
|
|
116
125
|
"test": "pnpm test:build",
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lifecycle/migrate-from-nextjs
|
|
3
|
+
description: >-
|
|
4
|
+
Step-by-step migration from Next.js App Router to TanStack Start:
|
|
5
|
+
route definition conversion, API mapping, server function
|
|
6
|
+
conversion from Server Actions, middleware conversion, data
|
|
7
|
+
fetching pattern changes.
|
|
8
|
+
type: lifecycle
|
|
9
|
+
library: tanstack-start
|
|
10
|
+
library_version: '1.166.2'
|
|
11
|
+
requires:
|
|
12
|
+
- start-core
|
|
13
|
+
- react-start
|
|
14
|
+
sources:
|
|
15
|
+
- TanStack/router:docs/start/framework/react/guide/server-functions.md
|
|
16
|
+
- TanStack/router:docs/start/framework/react/guide/middleware.md
|
|
17
|
+
- TanStack/router:docs/start/framework/react/guide/execution-model.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Migrate from Next.js App Router to TanStack Start
|
|
21
|
+
|
|
22
|
+
This is a step-by-step migration checklist. Complete tasks in order.
|
|
23
|
+
|
|
24
|
+
> **CRITICAL**: TanStack Start is isomorphic by default. ALL code runs in both environments unless you use `createServerFn`. This is the opposite of Next.js Server Components, where code is server-only by default.
|
|
25
|
+
|
|
26
|
+
> **CRITICAL**: TanStack Start uses `createServerFn`, NOT `"use server"` directives. Do not carry over any `"use server"` or `"use client"` directives.
|
|
27
|
+
|
|
28
|
+
> **CRITICAL**: Types are FULLY INFERRED in TanStack Router/Start. Never cast, never annotate inferred values.
|
|
29
|
+
|
|
30
|
+
## Pre-Migration
|
|
31
|
+
|
|
32
|
+
- [ ] **Create a migration branch**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git checkout -b migrate-to-tanstack-start
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- [ ] **Install TanStack Start**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm i @tanstack/react-start @tanstack/react-router
|
|
42
|
+
npm i -D vite @vitejs/plugin-react
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- [ ] **Remove Next.js**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm uninstall next @next/font @next/image
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Concept Mapping
|
|
52
|
+
|
|
53
|
+
| Next.js App Router | TanStack Start |
|
|
54
|
+
| -------------------------------- | ------------------------------------------------------------------------- |
|
|
55
|
+
| `app/page.tsx` | `src/routes/index.tsx` |
|
|
56
|
+
| `app/layout.tsx` | `src/routes/__root.tsx` |
|
|
57
|
+
| `app/posts/[id]/page.tsx` | `src/routes/posts/$postId.tsx` |
|
|
58
|
+
| `app/api/users/route.ts` | `src/routes/api/users.ts` (server property) |
|
|
59
|
+
| `"use server"` + Server Actions | `createServerFn()` |
|
|
60
|
+
| `"use client"` | Not needed (everything is isomorphic) |
|
|
61
|
+
| Server Components (default) | All components are isomorphic; use `createServerFn` for server-only logic |
|
|
62
|
+
| `next/navigation` `useRouter` | `useRouter()` from `@tanstack/react-router` |
|
|
63
|
+
| `next/link` `Link` | `<Link>` from `@tanstack/react-router` |
|
|
64
|
+
| `next/head` or `metadata` export | `head` property on route |
|
|
65
|
+
| `middleware.ts` (edge) | `createMiddleware()` in `src/start.ts` |
|
|
66
|
+
| `next.config.js` | `vite.config.ts` with `tanstackStart()` |
|
|
67
|
+
| `generateStaticParams` | `prerender` config in `vite.config.ts` |
|
|
68
|
+
|
|
69
|
+
## Step 1: Vite Configuration
|
|
70
|
+
|
|
71
|
+
Replace `next.config.js` with:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// vite.config.ts
|
|
75
|
+
import { defineConfig } from 'vite'
|
|
76
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
77
|
+
import viteReact from '@vitejs/plugin-react'
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
plugins: [
|
|
81
|
+
tanstackStart(), // MUST come before react()
|
|
82
|
+
viteReact(),
|
|
83
|
+
],
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Update `package.json`:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"type": "module",
|
|
92
|
+
"scripts": {
|
|
93
|
+
"dev": "vite dev",
|
|
94
|
+
"build": "vite build",
|
|
95
|
+
"start": "node .output/server/index.mjs"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Step 2: Router Factory
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// src/router.tsx
|
|
104
|
+
import { createRouter } from '@tanstack/react-router'
|
|
105
|
+
import { routeTree } from './routeTree.gen'
|
|
106
|
+
|
|
107
|
+
export function getRouter() {
|
|
108
|
+
const router = createRouter({
|
|
109
|
+
routeTree,
|
|
110
|
+
scrollRestoration: true,
|
|
111
|
+
})
|
|
112
|
+
return router
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Step 3: Convert Layout → Root Route
|
|
117
|
+
|
|
118
|
+
Next.js:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// app/layout.tsx
|
|
122
|
+
export const metadata = { title: 'My App' }
|
|
123
|
+
export default function RootLayout({ children }) {
|
|
124
|
+
return (
|
|
125
|
+
<html>
|
|
126
|
+
<body>{children}</body>
|
|
127
|
+
</html>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
TanStack Start:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
// src/routes/__root.tsx
|
|
136
|
+
import type { ReactNode } from 'react'
|
|
137
|
+
import {
|
|
138
|
+
Outlet,
|
|
139
|
+
createRootRoute,
|
|
140
|
+
HeadContent,
|
|
141
|
+
Scripts,
|
|
142
|
+
} from '@tanstack/react-router'
|
|
143
|
+
|
|
144
|
+
export const Route = createRootRoute({
|
|
145
|
+
head: () => ({
|
|
146
|
+
meta: [
|
|
147
|
+
{ charSet: 'utf-8' },
|
|
148
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
149
|
+
{ title: 'My App' },
|
|
150
|
+
],
|
|
151
|
+
}),
|
|
152
|
+
component: RootComponent,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
function RootComponent() {
|
|
156
|
+
return (
|
|
157
|
+
<html>
|
|
158
|
+
<head>
|
|
159
|
+
<HeadContent />
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
<Outlet />
|
|
163
|
+
<Scripts />
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Step 4: Convert Pages → File Routes
|
|
171
|
+
|
|
172
|
+
Next.js:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
// app/posts/[id]/page.tsx
|
|
176
|
+
export default function PostPage({ params }: { params: { id: string } }) {
|
|
177
|
+
// ...
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
TanStack Start:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// src/routes/posts/$postId.tsx
|
|
185
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
186
|
+
|
|
187
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
188
|
+
component: PostPage,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
function PostPage() {
|
|
192
|
+
const { postId } = Route.useParams()
|
|
193
|
+
// ...
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Key differences:
|
|
198
|
+
|
|
199
|
+
- Dynamic segments use `$param` not `[param]`
|
|
200
|
+
- Params accessed via `Route.useParams()` not component props
|
|
201
|
+
- Route path in filename uses `.` or `/` separators
|
|
202
|
+
|
|
203
|
+
## Step 5: Convert Server Actions → Server Functions
|
|
204
|
+
|
|
205
|
+
Next.js:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
// app/actions.ts
|
|
209
|
+
'use server'
|
|
210
|
+
export async function createPost(formData: FormData) {
|
|
211
|
+
const title = formData.get('title') as string
|
|
212
|
+
await db.posts.create({ title })
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
TanStack Start:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// src/utils/posts.functions.ts
|
|
220
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
221
|
+
|
|
222
|
+
export const createPost = createServerFn({ method: 'POST' })
|
|
223
|
+
.inputValidator((data) => {
|
|
224
|
+
if (!(data instanceof FormData)) throw new Error('Expected FormData')
|
|
225
|
+
return { title: data.get('title')?.toString() || '' }
|
|
226
|
+
})
|
|
227
|
+
.handler(async ({ data }) => {
|
|
228
|
+
await db.posts.create({ title: data.title })
|
|
229
|
+
return { success: true }
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Step 6: Convert Data Fetching
|
|
234
|
+
|
|
235
|
+
Next.js Server Component:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// app/posts/page.tsx (Server Component — server-only by default)
|
|
239
|
+
export default async function PostsPage() {
|
|
240
|
+
const posts = await db.posts.findMany()
|
|
241
|
+
return <PostList posts={posts} />
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
TanStack Start:
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
// src/routes/posts.tsx
|
|
249
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
250
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
251
|
+
|
|
252
|
+
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
|
|
253
|
+
return db.posts.findMany()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
export const Route = createFileRoute('/posts')({
|
|
257
|
+
loader: () => getPosts(), // loader is isomorphic, getPosts runs on server
|
|
258
|
+
component: PostsPage,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
function PostsPage() {
|
|
262
|
+
const posts = Route.useLoaderData()
|
|
263
|
+
return <PostList posts={posts} />
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Step 7: Convert API Routes → Server Routes
|
|
268
|
+
|
|
269
|
+
Next.js:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
// app/api/users/route.ts
|
|
273
|
+
export async function GET() {
|
|
274
|
+
const users = await db.users.findMany()
|
|
275
|
+
return Response.json(users)
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
TanStack Start:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
// src/routes/api/users.ts
|
|
283
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
284
|
+
|
|
285
|
+
export const Route = createFileRoute('/api/users')({
|
|
286
|
+
server: {
|
|
287
|
+
handlers: {
|
|
288
|
+
GET: async () => {
|
|
289
|
+
const users = await db.users.findMany()
|
|
290
|
+
return Response.json(users)
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Step 8: Convert Navigation
|
|
298
|
+
|
|
299
|
+
Next.js:
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import Link from 'next/link'
|
|
303
|
+
;<Link href={`/posts/${post.id}`}>View Post</Link>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
TanStack Start:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { Link } from '@tanstack/react-router'
|
|
310
|
+
;<Link to="/posts/$postId" params={{ postId: post.id }}>
|
|
311
|
+
View Post
|
|
312
|
+
</Link>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Never interpolate params into the `to` string. Use `params` prop.
|
|
316
|
+
|
|
317
|
+
## Step 9: Convert Middleware
|
|
318
|
+
|
|
319
|
+
Next.js:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
// middleware.ts
|
|
323
|
+
export function middleware(request: NextRequest) {
|
|
324
|
+
const token = request.cookies.get('session')
|
|
325
|
+
if (!token) return NextResponse.redirect(new URL('/login', request.url))
|
|
326
|
+
}
|
|
327
|
+
export const config = { matcher: ['/dashboard/:path*'] }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
TanStack Start:
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
// src/start.ts — must be manually created
|
|
334
|
+
import { createStart, createMiddleware } from '@tanstack/react-start'
|
|
335
|
+
import { redirect } from '@tanstack/react-router'
|
|
336
|
+
|
|
337
|
+
const authMiddleware = createMiddleware().server(async ({ next, request }) => {
|
|
338
|
+
const cookie = request.headers.get('cookie')
|
|
339
|
+
if (!cookie?.includes('session=')) {
|
|
340
|
+
throw redirect({ to: '/login' })
|
|
341
|
+
}
|
|
342
|
+
return next()
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
export const startInstance = createStart(() => ({
|
|
346
|
+
requestMiddleware: [authMiddleware],
|
|
347
|
+
}))
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Step 10: Convert Metadata/SEO
|
|
351
|
+
|
|
352
|
+
Next.js:
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
export const metadata = {
|
|
356
|
+
title: 'Post Title',
|
|
357
|
+
description: 'Post description',
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
TanStack Start:
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
365
|
+
loader: async ({ params }) => fetchPost(params.postId),
|
|
366
|
+
head: ({ loaderData }) => ({
|
|
367
|
+
meta: [
|
|
368
|
+
{ title: loaderData.title },
|
|
369
|
+
{ name: 'description', content: loaderData.excerpt },
|
|
370
|
+
{ property: 'og:title', content: loaderData.title },
|
|
371
|
+
],
|
|
372
|
+
}),
|
|
373
|
+
})
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Post-Migration Checklist
|
|
377
|
+
|
|
378
|
+
- [ ] Remove all `"use server"` and `"use client"` directives
|
|
379
|
+
- [ ] Remove `next.config.js` / `next.config.ts`
|
|
380
|
+
- [ ] Remove `app/` directory (replaced by `src/routes/`)
|
|
381
|
+
- [ ] Remove `middleware.ts` (replaced by `src/start.ts`)
|
|
382
|
+
- [ ] Verify no `next/*` imports remain
|
|
383
|
+
- [ ] Run `npm run dev` and check all routes
|
|
384
|
+
- [ ] Verify server-only code is inside `createServerFn` (not bare in components/loaders)
|
|
385
|
+
- [ ] Check that `<Scripts />` is in the root route `<body>`
|
|
386
|
+
|
|
387
|
+
## Common Mistakes
|
|
388
|
+
|
|
389
|
+
### 1. CRITICAL: Keeping Server Component mental model
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
// WRONG — treating component as server-only (Next.js habit)
|
|
393
|
+
function PostsPage() {
|
|
394
|
+
const posts = await db.posts.findMany() // fails on client
|
|
395
|
+
return <div>{posts.map(...)}</div>
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// CORRECT — use server function + loader
|
|
399
|
+
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
|
|
400
|
+
return db.posts.findMany()
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
export const Route = createFileRoute('/posts')({
|
|
404
|
+
loader: () => getPosts(),
|
|
405
|
+
component: PostsPage,
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### 2. CRITICAL: Using "use server" directive
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
// WRONG — "use server" is Next.js/React pattern
|
|
413
|
+
'use server'
|
|
414
|
+
export async function myAction() { ... }
|
|
415
|
+
|
|
416
|
+
// CORRECT — use createServerFn
|
|
417
|
+
export const myAction = createServerFn({ method: 'POST' })
|
|
418
|
+
.handler(async () => { ... })
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### 3. HIGH: Interpolating params into Link href
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
// WRONG — Next.js pattern
|
|
425
|
+
<Link to={`/posts/${post.id}`}>View</Link>
|
|
426
|
+
|
|
427
|
+
// CORRECT — TanStack Router pattern
|
|
428
|
+
<Link to="/posts/$postId" params={{ postId: post.id }}>View</Link>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Cross-References
|
|
432
|
+
|
|
433
|
+
- [react-start](../../react-start/SKILL.md) — full React Start setup
|
|
434
|
+
- [start-core/server-functions](../../../../start-client-core/skills/start-core/server-functions/SKILL.md) — server function patterns
|
|
435
|
+
- [start-core/execution-model](../../../../start-client-core/skills/start-core/execution-model/SKILL.md) — isomorphic execution
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-start
|
|
3
|
+
description: >-
|
|
4
|
+
React bindings for TanStack Start: createStart, StartClient,
|
|
5
|
+
StartServer, React-specific imports, re-exports from
|
|
6
|
+
@tanstack/react-router, full project setup with React, useServerFn
|
|
7
|
+
hook.
|
|
8
|
+
type: framework
|
|
9
|
+
library: tanstack-start
|
|
10
|
+
library_version: '1.166.2'
|
|
11
|
+
framework: react
|
|
12
|
+
requires:
|
|
13
|
+
- start-core
|
|
14
|
+
sources:
|
|
15
|
+
- TanStack/router:packages/react-start/src
|
|
16
|
+
- TanStack/router:docs/start/framework/react/build-from-scratch.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# React Start (`@tanstack/react-start`)
|
|
20
|
+
|
|
21
|
+
This skill builds on start-core. Read [start-core](../../../start-client-core/skills/start-core/SKILL.md) first for foundational concepts.
|
|
22
|
+
|
|
23
|
+
This skill covers the React-specific bindings, setup, and patterns for TanStack Start.
|
|
24
|
+
|
|
25
|
+
> **CRITICAL**: All code is ISOMORPHIC by default. Loaders run on BOTH server and client. Use `createServerFn` for server-only logic.
|
|
26
|
+
|
|
27
|
+
> **CRITICAL**: Do not confuse `@tanstack/react-start` with Next.js or Remix. They are completely different frameworks with different APIs.
|
|
28
|
+
|
|
29
|
+
> **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values.
|
|
30
|
+
|
|
31
|
+
## Package API Surface
|
|
32
|
+
|
|
33
|
+
`@tanstack/react-start` re-exports everything from `@tanstack/start-client-core` plus:
|
|
34
|
+
|
|
35
|
+
- `useServerFn` — React hook for calling server functions from components
|
|
36
|
+
|
|
37
|
+
All core APIs (`createServerFn`, `createMiddleware`, `createStart`, `createIsomorphicFn`, `createServerOnlyFn`, `createClientOnlyFn`) are available from `@tanstack/react-start`.
|
|
38
|
+
|
|
39
|
+
Server utilities (`getRequest`, `getRequestHeader`, `setResponseHeader`, `setResponseHeaders`, `setResponseStatus`) are imported from `@tanstack/react-start/server`.
|
|
40
|
+
|
|
41
|
+
## Full Project Setup
|
|
42
|
+
|
|
43
|
+
### 1. Install Dependencies
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm i @tanstack/react-start @tanstack/react-router react react-dom
|
|
47
|
+
npm i -D vite @vitejs/plugin-react typescript @types/react @types/react-dom
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. package.json
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"type": "module",
|
|
55
|
+
"scripts": {
|
|
56
|
+
"dev": "vite dev",
|
|
57
|
+
"build": "vite build",
|
|
58
|
+
"start": "node .output/server/index.mjs"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. tsconfig.json
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"compilerOptions": {
|
|
68
|
+
"jsx": "react-jsx",
|
|
69
|
+
"moduleResolution": "Bundler",
|
|
70
|
+
"module": "ESNext",
|
|
71
|
+
"target": "ES2022",
|
|
72
|
+
"skipLibCheck": true,
|
|
73
|
+
"strictNullChecks": true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 4. vite.config.ts
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { defineConfig } from 'vite'
|
|
82
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
83
|
+
import viteReact from '@vitejs/plugin-react'
|
|
84
|
+
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
plugins: [
|
|
87
|
+
tanstackStart(), // MUST come before react()
|
|
88
|
+
viteReact(),
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 5. Router Factory (src/router.tsx)
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { createRouter } from '@tanstack/react-router'
|
|
97
|
+
import { routeTree } from './routeTree.gen'
|
|
98
|
+
|
|
99
|
+
export function getRouter() {
|
|
100
|
+
const router = createRouter({
|
|
101
|
+
routeTree,
|
|
102
|
+
scrollRestoration: true,
|
|
103
|
+
})
|
|
104
|
+
return router
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 6. Root Route (src/routes/\_\_root.tsx)
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import type { ReactNode } from 'react'
|
|
112
|
+
import {
|
|
113
|
+
Outlet,
|
|
114
|
+
createRootRoute,
|
|
115
|
+
HeadContent,
|
|
116
|
+
Scripts,
|
|
117
|
+
} from '@tanstack/react-router'
|
|
118
|
+
|
|
119
|
+
export const Route = createRootRoute({
|
|
120
|
+
head: () => ({
|
|
121
|
+
meta: [
|
|
122
|
+
{ charSet: 'utf-8' },
|
|
123
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
124
|
+
{ title: 'My TanStack Start App' },
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
component: RootComponent,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
function RootComponent() {
|
|
131
|
+
return (
|
|
132
|
+
<RootDocument>
|
|
133
|
+
<Outlet />
|
|
134
|
+
</RootDocument>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
|
139
|
+
return (
|
|
140
|
+
<html>
|
|
141
|
+
<head>
|
|
142
|
+
<HeadContent />
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
{children}
|
|
146
|
+
<Scripts />
|
|
147
|
+
</body>
|
|
148
|
+
</html>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 7. Index Route (src/routes/index.tsx)
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
157
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
158
|
+
|
|
159
|
+
const getGreeting = createServerFn({ method: 'GET' }).handler(async () => {
|
|
160
|
+
return 'Hello from TanStack Start!'
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
export const Route = createFileRoute('/')({
|
|
164
|
+
loader: () => getGreeting(),
|
|
165
|
+
component: HomePage,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
function HomePage() {
|
|
169
|
+
const greeting = Route.useLoaderData()
|
|
170
|
+
return <h1>{greeting}</h1>
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## useServerFn Hook
|
|
175
|
+
|
|
176
|
+
Use `useServerFn` to call server functions from React components with proper integration:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { createServerFn, useServerFn } from '@tanstack/react-start'
|
|
180
|
+
|
|
181
|
+
const updatePost = createServerFn({ method: 'POST' })
|
|
182
|
+
.inputValidator((data: { id: string; title: string }) => data)
|
|
183
|
+
.handler(async ({ data }) => {
|
|
184
|
+
await db.posts.update(data.id, { title: data.title })
|
|
185
|
+
return { success: true }
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
function EditPostForm({ postId }: { postId: string }) {
|
|
189
|
+
const updatePostFn = useServerFn(updatePost)
|
|
190
|
+
const [title, setTitle] = useState('')
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<form
|
|
194
|
+
onSubmit={async (e) => {
|
|
195
|
+
e.preventDefault()
|
|
196
|
+
await updatePostFn({ data: { id: postId, title } })
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<input value={title} onChange={(e) => setTitle(e.target.value)} />
|
|
200
|
+
<button type="submit">Save</button>
|
|
201
|
+
</form>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Global Start Configuration (src/start.ts)
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
import { createStart, createMiddleware } from '@tanstack/react-start'
|
|
210
|
+
|
|
211
|
+
const requestLogger = createMiddleware().server(async ({ next, request }) => {
|
|
212
|
+
console.log(`${request.method} ${request.url}`)
|
|
213
|
+
return next()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
export const startInstance = createStart(() => ({
|
|
217
|
+
requestMiddleware: [requestLogger],
|
|
218
|
+
}))
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## React-Specific Components
|
|
222
|
+
|
|
223
|
+
All routing components from `@tanstack/react-router` work in Start:
|
|
224
|
+
|
|
225
|
+
- `<RouterProvider>` — not needed in Start (handled automatically)
|
|
226
|
+
- `<Outlet>` — renders matched child route
|
|
227
|
+
- `<Link>` — type-safe navigation
|
|
228
|
+
- `<Navigate>` — declarative redirect
|
|
229
|
+
- `<HeadContent>` — renders head tags (must be in `<head>`)
|
|
230
|
+
- `<Scripts>` — renders body scripts (must be in `<body>`)
|
|
231
|
+
- `<Await>` — renders deferred data with Suspense
|
|
232
|
+
- `<ClientOnly>` — renders children only after hydration
|
|
233
|
+
- `<CatchBoundary>` — error boundary
|
|
234
|
+
|
|
235
|
+
## Hooks Reference
|
|
236
|
+
|
|
237
|
+
All hooks from `@tanstack/react-router` work in Start:
|
|
238
|
+
|
|
239
|
+
- `useRouter()` — router instance
|
|
240
|
+
- `useRouterState()` — subscribe to router state
|
|
241
|
+
- `useNavigate()` — programmatic navigation
|
|
242
|
+
- `useSearch({ from })` — validated search params
|
|
243
|
+
- `useParams({ from })` — path params
|
|
244
|
+
- `useLoaderData({ from })` — loader data
|
|
245
|
+
- `useMatch({ from })` — full route match
|
|
246
|
+
- `useRouteContext({ from })` — route context
|
|
247
|
+
- `Route.useLoaderData()` — typed loader data (preferred in route files)
|
|
248
|
+
- `Route.useSearch()` — typed search params (preferred in route files)
|
|
249
|
+
|
|
250
|
+
## Common Mistakes
|
|
251
|
+
|
|
252
|
+
### 1. CRITICAL: Importing from wrong package
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
// WRONG — this is the SPA router, NOT Start
|
|
256
|
+
import { createServerFn } from '@tanstack/react-router'
|
|
257
|
+
|
|
258
|
+
// CORRECT — server functions come from react-start
|
|
259
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
260
|
+
|
|
261
|
+
// CORRECT — routing APIs come from react-router (re-exported by Start too)
|
|
262
|
+
import { createFileRoute, Link } from '@tanstack/react-router'
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 2. HIGH: Using React hooks in beforeLoad or loader
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
// WRONG — beforeLoad/loader are NOT React components
|
|
269
|
+
beforeLoad: () => {
|
|
270
|
+
const auth = useAuth() // React hook, cannot be used here
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// CORRECT — pass state via router context
|
|
274
|
+
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 3. HIGH: Missing Scripts component
|
|
278
|
+
|
|
279
|
+
Without `<Scripts />` in the root route's `<body>`, client JavaScript doesn't load and the app won't hydrate.
|
|
280
|
+
|
|
281
|
+
## Cross-References
|
|
282
|
+
|
|
283
|
+
- [start-core](../../../start-client-core/skills/start-core/SKILL.md) — core Start concepts
|
|
284
|
+
- [router-core](../../../router-core/skills/router-core/SKILL.md) — routing fundamentals
|
|
285
|
+
- [react-router](../../../react-router/skills/react-router/SKILL.md) — React Router hooks and components
|