@tanstack/router-core 1.167.3 → 1.167.5
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 +10 -3
- package/skills/router-core/SKILL.md +139 -0
- package/skills/router-core/auth-and-guards/SKILL.md +458 -0
- package/skills/router-core/code-splitting/SKILL.md +322 -0
- package/skills/router-core/data-loading/SKILL.md +485 -0
- package/skills/router-core/navigation/SKILL.md +448 -0
- package/skills/router-core/not-found-and-errors/SKILL.md +435 -0
- package/skills/router-core/path-params/SKILL.md +382 -0
- package/skills/router-core/search-params/SKILL.md +355 -0
- package/skills/router-core/search-params/references/validation-patterns.md +379 -0
- package/skills/router-core/ssr/SKILL.md +437 -0
- package/skills/router-core/type-safety/SKILL.md +497 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: router-core/path-params
|
|
3
|
+
description: >-
|
|
4
|
+
Dynamic path segments ($paramName), splat routes ($ / _splat),
|
|
5
|
+
optional params ({-$paramName}), prefix/suffix patterns ({$param}.ext),
|
|
6
|
+
useParams, params.parse/stringify, pathParamsAllowedCharacters,
|
|
7
|
+
i18n locale patterns.
|
|
8
|
+
type: sub-skill
|
|
9
|
+
library: tanstack-router
|
|
10
|
+
library_version: '1.166.2'
|
|
11
|
+
requires:
|
|
12
|
+
- router-core
|
|
13
|
+
sources:
|
|
14
|
+
- TanStack/router:docs/router/guide/path-params.md
|
|
15
|
+
- TanStack/router:docs/router/routing/routing-concepts.md
|
|
16
|
+
- TanStack/router:docs/router/guide/internationalization-i18n.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Path Params
|
|
20
|
+
|
|
21
|
+
Path params capture dynamic URL segments into named variables. They are defined with a `$` prefix in the route path.
|
|
22
|
+
|
|
23
|
+
> **CRITICAL**: Never interpolate params into the `to` string. Always use the `params` prop. This is the most common agent mistake for path params.
|
|
24
|
+
|
|
25
|
+
> **CRITICAL**: Types are fully inferred. Never annotate the return of `useParams()`.
|
|
26
|
+
|
|
27
|
+
## Dynamic Segments
|
|
28
|
+
|
|
29
|
+
A segment prefixed with `$` captures text until the next `/`.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// src/routes/posts.$postId.tsx
|
|
33
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
34
|
+
|
|
35
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
36
|
+
loader: async ({ params }) => {
|
|
37
|
+
// params.postId is string — fully inferred, do not annotate
|
|
38
|
+
return fetchPost(params.postId)
|
|
39
|
+
},
|
|
40
|
+
component: PostComponent,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
function PostComponent() {
|
|
44
|
+
const { postId } = Route.useParams()
|
|
45
|
+
const data = Route.useLoaderData()
|
|
46
|
+
return (
|
|
47
|
+
<h1>
|
|
48
|
+
Post {postId}: {data.title}
|
|
49
|
+
</h1>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Multiple dynamic segments work across path levels:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// src/routes/teams.$teamId.members.$memberId.tsx
|
|
58
|
+
export const Route = createFileRoute('/teams/$teamId/members/$memberId')({
|
|
59
|
+
component: MemberComponent,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
function MemberComponent() {
|
|
63
|
+
const { teamId, memberId } = Route.useParams()
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
Team {teamId}, Member {memberId}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Splat / Catch-All Routes
|
|
73
|
+
|
|
74
|
+
A route with a path ending in `$` (bare dollar sign) captures everything after it. The value is available under the `_splat` key.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// src/routes/files.$.tsx
|
|
78
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
79
|
+
|
|
80
|
+
export const Route = createFileRoute('/files/$')({
|
|
81
|
+
component: FileViewer,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
function FileViewer() {
|
|
85
|
+
const { _splat } = Route.useParams()
|
|
86
|
+
// URL: /files/documents/report.pdf → _splat = "documents/report.pdf"
|
|
87
|
+
return <div>File path: {_splat}</div>
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Optional Params
|
|
92
|
+
|
|
93
|
+
Optional params use `{-$paramName}` syntax. The segment may or may not be present. When absent, the value is `undefined`.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// src/routes/posts.{-$category}.tsx
|
|
97
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
98
|
+
|
|
99
|
+
export const Route = createFileRoute('/posts/{-$category}')({
|
|
100
|
+
component: PostsComponent,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
function PostsComponent() {
|
|
104
|
+
const { category } = Route.useParams()
|
|
105
|
+
// URL: /posts → category is undefined
|
|
106
|
+
// URL: /posts/tech → category is "tech"
|
|
107
|
+
return <div>{category ? `Posts in ${category}` : 'All Posts'}</div>
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Multiple optional params:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// Matches: /posts, /posts/tech, /posts/tech/hello-world
|
|
115
|
+
export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({
|
|
116
|
+
component: PostComponent,
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### i18n with Optional Locale
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// src/routes/{-$locale}/about.tsx
|
|
124
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
125
|
+
|
|
126
|
+
export const Route = createFileRoute('/{-$locale}/about')({
|
|
127
|
+
component: AboutComponent,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
function AboutComponent() {
|
|
131
|
+
const { locale } = Route.useParams()
|
|
132
|
+
const currentLocale = locale || 'en'
|
|
133
|
+
return <h1>{currentLocale === 'fr' ? 'À Propos' : 'About Us'}</h1>
|
|
134
|
+
}
|
|
135
|
+
// Matches: /about, /en/about, /fr/about
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Prefix and Suffix Patterns
|
|
139
|
+
|
|
140
|
+
Curly braces `{}` around `$paramName` allow text before or after the dynamic part within a single segment.
|
|
141
|
+
|
|
142
|
+
### Prefix
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// src/routes/posts/post-{$postId}.tsx
|
|
146
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
147
|
+
|
|
148
|
+
export const Route = createFileRoute('/posts/post-{$postId}')({
|
|
149
|
+
component: PostComponent,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
function PostComponent() {
|
|
153
|
+
const { postId } = Route.useParams()
|
|
154
|
+
// URL: /posts/post-123 → postId = "123"
|
|
155
|
+
return <div>Post ID: {postId}</div>
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Suffix
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
// src/routes/files/{$fileName}[.]txt.tsx
|
|
163
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
164
|
+
|
|
165
|
+
export const Route = createFileRoute('/files/{$fileName}.txt')({
|
|
166
|
+
component: FileComponent,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
function FileComponent() {
|
|
170
|
+
const { fileName } = Route.useParams()
|
|
171
|
+
// URL: /files/readme.txt → fileName = "readme"
|
|
172
|
+
return <div>File: {fileName}.txt</div>
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Combined Prefix + Suffix
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// URL: /users/user-456.json → userId = "456"
|
|
180
|
+
export const Route = createFileRoute('/users/user-{$userId}.json')({
|
|
181
|
+
component: UserComponent,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
function UserComponent() {
|
|
185
|
+
const { userId } = Route.useParams()
|
|
186
|
+
return <div>User: {userId}</div>
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Navigating with Path Params
|
|
191
|
+
|
|
192
|
+
### Object Form
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { Link } from '@tanstack/react-router'
|
|
196
|
+
|
|
197
|
+
function PostLink({ postId }: { postId: string }) {
|
|
198
|
+
return (
|
|
199
|
+
<Link to="/posts/$postId" params={{ postId }}>
|
|
200
|
+
View Post
|
|
201
|
+
</Link>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Function Form (Preserves Other Params)
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
function PostLink({ postId }: { postId: string }) {
|
|
210
|
+
return (
|
|
211
|
+
<Link to="/posts/$postId" params={(prev) => ({ ...prev, postId })}>
|
|
212
|
+
View Post
|
|
213
|
+
</Link>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Programmatic Navigation
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
222
|
+
|
|
223
|
+
function GoToPost({ postId }: { postId: string }) {
|
|
224
|
+
const navigate = useNavigate()
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => {
|
|
229
|
+
navigate({ to: '/posts/$postId', params: { postId } })
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
Go to Post
|
|
233
|
+
</button>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Navigating with Optional Params
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
// Include the optional param
|
|
242
|
+
<Link to="/posts/{-$category}" params={{ category: 'tech' }}>
|
|
243
|
+
Tech Posts
|
|
244
|
+
</Link>
|
|
245
|
+
|
|
246
|
+
// Omit the optional param (renders /posts)
|
|
247
|
+
<Link to="/posts/{-$category}" params={{ category: undefined }}>
|
|
248
|
+
All Posts
|
|
249
|
+
</Link>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Reading Params Outside Route Components
|
|
253
|
+
|
|
254
|
+
### `useParams` with `from`
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
import { useParams } from '@tanstack/react-router'
|
|
258
|
+
|
|
259
|
+
function PostHeader() {
|
|
260
|
+
const { postId } = useParams({ from: '/posts/$postId' })
|
|
261
|
+
return <h2>Post {postId}</h2>
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### `useParams` with `strict: false`
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
function GenericBreadcrumb() {
|
|
269
|
+
const params = useParams({ strict: false })
|
|
270
|
+
// params is a union of all possible route params
|
|
271
|
+
return <span>{params.postId ?? 'Home'}</span>
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Params in Loaders and `beforeLoad`
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
279
|
+
beforeLoad: async ({ params }) => {
|
|
280
|
+
// params.postId available here
|
|
281
|
+
const canView = await checkPermission(params.postId)
|
|
282
|
+
if (!canView) throw redirect({ to: '/unauthorized' })
|
|
283
|
+
},
|
|
284
|
+
loader: async ({ params }) => {
|
|
285
|
+
return fetchPost(params.postId)
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Allowed Characters
|
|
291
|
+
|
|
292
|
+
By default, params are encoded with `encodeURIComponent`. Allow extra characters via router config:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
import { createRouter } from '@tanstack/react-router'
|
|
296
|
+
|
|
297
|
+
const router = createRouter({
|
|
298
|
+
routeTree,
|
|
299
|
+
pathParamsAllowedCharacters: ['@', '+'],
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Allowed characters: `;`, `:`, `@`, `&`, `=`, `+`, `$`, `,`.
|
|
304
|
+
|
|
305
|
+
## Common Mistakes
|
|
306
|
+
|
|
307
|
+
### 1. CRITICAL (cross-skill): Interpolating path params into `to` string
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
// WRONG — breaks type safety and param encoding
|
|
311
|
+
<Link to={`/posts/${postId}`}>Post</Link>
|
|
312
|
+
|
|
313
|
+
// CORRECT — use params prop
|
|
314
|
+
<Link to="/posts/$postId" params={{ postId }}>Post</Link>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### 2. MEDIUM: Using `*` for splat routes instead of `$`
|
|
318
|
+
|
|
319
|
+
TanStack Router uses `$` for splat routes. The captured value is under `_splat`, not `*`.
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
// WRONG (React Router / other frameworks)
|
|
323
|
+
// <Route path="/files/*" />
|
|
324
|
+
|
|
325
|
+
// CORRECT (TanStack Router)
|
|
326
|
+
// File: src/routes/files.$.tsx
|
|
327
|
+
export const Route = createFileRoute('/files/$')({
|
|
328
|
+
component: () => {
|
|
329
|
+
const { _splat } = Route.useParams()
|
|
330
|
+
return <div>{_splat}</div>
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
> Note: `*` works in v1 for backwards compatibility but will be removed in v2. Always use `_splat`.
|
|
336
|
+
|
|
337
|
+
### 3. MEDIUM: Using curly braces for basic dynamic segments
|
|
338
|
+
|
|
339
|
+
Curly braces are ONLY for prefix/suffix patterns and optional params. Basic dynamic segments use bare `$`.
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
// WRONG — braces not needed for basic params
|
|
343
|
+
createFileRoute('/posts/{$postId}')
|
|
344
|
+
|
|
345
|
+
// CORRECT — bare $ for basic dynamic segments
|
|
346
|
+
createFileRoute('/posts/$postId')
|
|
347
|
+
|
|
348
|
+
// CORRECT — braces for prefix pattern
|
|
349
|
+
createFileRoute('/posts/post-{$postId}')
|
|
350
|
+
|
|
351
|
+
// CORRECT — braces for optional param
|
|
352
|
+
createFileRoute('/posts/{-$category}')
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### 4. Params are always strings
|
|
356
|
+
|
|
357
|
+
Path params are always parsed as strings. If you need a number, parse in the loader or component:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
361
|
+
loader: async ({ params }) => {
|
|
362
|
+
const id = parseInt(params.postId, 10)
|
|
363
|
+
if (isNaN(id)) throw notFound()
|
|
364
|
+
return fetchPost(id)
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
You can also use `params.parse` and `params.stringify` on the route for bidirectional transformation:
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
373
|
+
params: {
|
|
374
|
+
parse: (raw) => ({ postId: parseInt(raw.postId, 10) }),
|
|
375
|
+
stringify: (parsed) => ({ postId: String(parsed.postId) }),
|
|
376
|
+
},
|
|
377
|
+
loader: async ({ params }) => {
|
|
378
|
+
// params.postId is now number
|
|
379
|
+
return fetchPost(params.postId)
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
```
|