@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.
@@ -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
+ ```