@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,435 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: router-core/not-found-and-errors
|
|
3
|
+
description: >-
|
|
4
|
+
notFound() function, notFoundComponent, defaultNotFoundComponent,
|
|
5
|
+
notFoundMode (fuzzy/root), errorComponent, CatchBoundary,
|
|
6
|
+
CatchNotFound, isNotFound, NotFoundRoute (deprecated), route
|
|
7
|
+
masking (mask option, createRouteMask, unmaskOnReload).
|
|
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/not-found-errors.md
|
|
15
|
+
- TanStack/router:docs/router/guide/route-masking.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Not Found and Errors
|
|
19
|
+
|
|
20
|
+
TanStack Router handles two categories of "not found": unmatched URL paths (automatic) and missing resources like a post that doesn't exist (manual via `notFound()`). Error boundaries are configured per-route via `errorComponent`.
|
|
21
|
+
|
|
22
|
+
> **CRITICAL**: Do NOT use the deprecated `NotFoundRoute`. When present, `notFound()` and `notFoundComponent` will NOT work. Remove it and use `notFoundComponent` instead.
|
|
23
|
+
> **CRITICAL**: `useLoaderData` may be undefined inside `notFoundComponent`. Use `useParams`, `useSearch`, or `useRouteContext` instead.
|
|
24
|
+
|
|
25
|
+
## Not Found Handling
|
|
26
|
+
|
|
27
|
+
### Global 404: `notFoundComponent` on Root Route
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// src/routes/__root.tsx
|
|
31
|
+
import { createRootRoute, Outlet, Link } from '@tanstack/react-router'
|
|
32
|
+
|
|
33
|
+
export const Route = createRootRoute({
|
|
34
|
+
component: () => <Outlet />,
|
|
35
|
+
notFoundComponent: () => {
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<h1>404 — Page Not Found</h1>
|
|
39
|
+
<Link to="/">Go Home</Link>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Router-Wide Default: `defaultNotFoundComponent`
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// src/router.tsx
|
|
50
|
+
import { createRouter } from '@tanstack/react-router'
|
|
51
|
+
import { routeTree } from './routeTree.gen'
|
|
52
|
+
|
|
53
|
+
const router = createRouter({
|
|
54
|
+
routeTree,
|
|
55
|
+
defaultNotFoundComponent: () => {
|
|
56
|
+
return (
|
|
57
|
+
<div>
|
|
58
|
+
<p>Not found!</p>
|
|
59
|
+
<Link to="/">Go home</Link>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Per-Route 404: Missing Resources with `notFound()`
|
|
67
|
+
|
|
68
|
+
Throw `notFound()` in `loader` or `beforeLoad` when a resource doesn't exist. It works like `redirect()` — throw it to trigger the not-found boundary.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// src/routes/posts.$postId.tsx
|
|
72
|
+
import { createFileRoute, notFound } from '@tanstack/react-router'
|
|
73
|
+
import { getPost } from '../api'
|
|
74
|
+
|
|
75
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
76
|
+
loader: async ({ params: { postId } }) => {
|
|
77
|
+
const post = await getPost(postId)
|
|
78
|
+
if (!post) throw notFound()
|
|
79
|
+
return { post }
|
|
80
|
+
},
|
|
81
|
+
component: PostComponent,
|
|
82
|
+
notFoundComponent: ({ data }) => {
|
|
83
|
+
const { postId } = Route.useParams()
|
|
84
|
+
return <p>Post "{postId}" not found</p>
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
function PostComponent() {
|
|
89
|
+
const { post } = Route.useLoaderData()
|
|
90
|
+
return <h1>{post.title}</h1>
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Targeting a Specific Route with `notFound({ routeId })`
|
|
95
|
+
|
|
96
|
+
You can force a specific parent route to handle the not-found error:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// src/routes/_layout/posts.$postId.tsx
|
|
100
|
+
import { createFileRoute, notFound } from '@tanstack/react-router'
|
|
101
|
+
|
|
102
|
+
export const Route = createFileRoute('/_layout/posts/$postId')({
|
|
103
|
+
loader: async ({ params: { postId } }) => {
|
|
104
|
+
const post = await getPost(postId)
|
|
105
|
+
if (!post) throw notFound({ routeId: '/_layout' })
|
|
106
|
+
return { post }
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Targeting Root Route with `rootRouteId`
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { createFileRoute, notFound, rootRouteId } from '@tanstack/react-router'
|
|
115
|
+
|
|
116
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
117
|
+
loader: async ({ params: { postId } }) => {
|
|
118
|
+
const post = await getPost(postId)
|
|
119
|
+
if (!post) throw notFound({ routeId: rootRouteId })
|
|
120
|
+
return { post }
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## `notFoundMode`: Fuzzy vs Root
|
|
126
|
+
|
|
127
|
+
### `fuzzy` (default)
|
|
128
|
+
|
|
129
|
+
The router finds the nearest parent route with children and a `notFoundComponent`. Preserves as much parent layout as possible.
|
|
130
|
+
|
|
131
|
+
Given routes: `__root__` → `posts` → `$postId`, accessing `/posts/1/edit`:
|
|
132
|
+
|
|
133
|
+
- `<Root>` renders
|
|
134
|
+
- `<Posts>` renders
|
|
135
|
+
- `<Posts.notFoundComponent>` renders (nearest parent with children + notFoundComponent)
|
|
136
|
+
|
|
137
|
+
### `root`
|
|
138
|
+
|
|
139
|
+
All not-found errors go to the root route's `notFoundComponent`, regardless of matching:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
const router = createRouter({
|
|
143
|
+
routeTree,
|
|
144
|
+
notFoundMode: 'root',
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Error Handling
|
|
149
|
+
|
|
150
|
+
### `errorComponent` Per Route
|
|
151
|
+
|
|
152
|
+
`errorComponent` receives `error`, `info`, and `reset` props. For loader errors, use `router.invalidate()` to re-run the loader — it automatically resets the error boundary.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
// src/routes/posts.$postId.tsx
|
|
156
|
+
import { createFileRoute, useRouter } from '@tanstack/react-router'
|
|
157
|
+
|
|
158
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
159
|
+
loader: async ({ params: { postId } }) => {
|
|
160
|
+
const res = await fetch(`/api/posts/${postId}`)
|
|
161
|
+
if (!res.ok) throw new Error('Failed to load post')
|
|
162
|
+
return res.json()
|
|
163
|
+
},
|
|
164
|
+
component: PostComponent,
|
|
165
|
+
errorComponent: PostErrorComponent,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
function PostErrorComponent({
|
|
169
|
+
error,
|
|
170
|
+
}: {
|
|
171
|
+
error: Error
|
|
172
|
+
info: { componentStack: string }
|
|
173
|
+
reset: () => void
|
|
174
|
+
}) {
|
|
175
|
+
const router = useRouter()
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div>
|
|
179
|
+
<p>Error: {error.message}</p>
|
|
180
|
+
<button
|
|
181
|
+
onClick={() => {
|
|
182
|
+
// Invalidate re-runs the loader and resets the error boundary
|
|
183
|
+
router.invalidate()
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
Retry
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function PostComponent() {
|
|
193
|
+
const data = Route.useLoaderData()
|
|
194
|
+
return <h1>{data.title}</h1>
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Router-Wide Default Error Component
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
const router = createRouter({
|
|
202
|
+
routeTree,
|
|
203
|
+
defaultErrorComponent: ({ error }) => {
|
|
204
|
+
const router = useRouter()
|
|
205
|
+
return (
|
|
206
|
+
<div>
|
|
207
|
+
<p>Something went wrong: {error.message}</p>
|
|
208
|
+
<button
|
|
209
|
+
onClick={() => {
|
|
210
|
+
router.invalidate()
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
Retry
|
|
214
|
+
</button>
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Data in `notFoundComponent`
|
|
222
|
+
|
|
223
|
+
`notFoundComponent` cannot reliably use `useLoaderData` because the loader may not have completed. Safe hooks:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
notFoundComponent: ({ data }) => {
|
|
227
|
+
// SAFE — always available:
|
|
228
|
+
const params = Route.useParams()
|
|
229
|
+
const search = Route.useSearch()
|
|
230
|
+
const context = Route.useRouteContext()
|
|
231
|
+
|
|
232
|
+
// UNSAFE — may be undefined:
|
|
233
|
+
// const loaderData = Route.useLoaderData()
|
|
234
|
+
|
|
235
|
+
return <p>Item {params.id} not found</p>
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
To forward partial data, use the `data` option on `notFound()`:
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
loader: async ({ params }) => {
|
|
243
|
+
const partialData = await getPartialData(params.id)
|
|
244
|
+
if (!partialData.fullResource) {
|
|
245
|
+
throw notFound({ data: { name: partialData.name } })
|
|
246
|
+
}
|
|
247
|
+
return partialData
|
|
248
|
+
},
|
|
249
|
+
notFoundComponent: ({ data }) => {
|
|
250
|
+
// data is typed as unknown — validate it
|
|
251
|
+
const info = data as { name: string } | undefined
|
|
252
|
+
return <p>{info?.name ?? 'Resource'} not found</p>
|
|
253
|
+
},
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Route Masking
|
|
257
|
+
|
|
258
|
+
Route masking shows a different URL in the browser bar than the actual route being rendered. Masking data is stored in `location.state` and is lost when the URL is shared or opened in a new tab.
|
|
259
|
+
|
|
260
|
+
### Imperative Masking on `<Link>`
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
import { Link } from '@tanstack/react-router'
|
|
264
|
+
|
|
265
|
+
function PhotoGrid({ photoId }: { photoId: string }) {
|
|
266
|
+
return (
|
|
267
|
+
<Link
|
|
268
|
+
to="/photos/$photoId/modal"
|
|
269
|
+
params={{ photoId }}
|
|
270
|
+
mask={{
|
|
271
|
+
to: '/photos/$photoId',
|
|
272
|
+
params: { photoId },
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
Open Photo
|
|
276
|
+
</Link>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Imperative Masking with `useNavigate`
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
285
|
+
|
|
286
|
+
function OpenPhotoButton({ photoId }: { photoId: string }) {
|
|
287
|
+
const navigate = useNavigate()
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<button
|
|
291
|
+
onClick={() =>
|
|
292
|
+
navigate({
|
|
293
|
+
to: '/photos/$photoId/modal',
|
|
294
|
+
params: { photoId },
|
|
295
|
+
mask: {
|
|
296
|
+
to: '/photos/$photoId',
|
|
297
|
+
params: { photoId },
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
>
|
|
302
|
+
Open Photo
|
|
303
|
+
</button>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Declarative Masking with `createRouteMask`
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
import { createRouter, createRouteMask } from '@tanstack/react-router'
|
|
312
|
+
import { routeTree } from './routeTree.gen'
|
|
313
|
+
|
|
314
|
+
const photoModalMask = createRouteMask({
|
|
315
|
+
routeTree,
|
|
316
|
+
from: '/photos/$photoId/modal',
|
|
317
|
+
to: '/photos/$photoId',
|
|
318
|
+
params: (prev) => ({ photoId: prev.photoId }),
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const router = createRouter({
|
|
322
|
+
routeTree,
|
|
323
|
+
routeMasks: [photoModalMask],
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Unmasking on Reload
|
|
328
|
+
|
|
329
|
+
By default, masks survive local page reloads. To unmask on reload:
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
// Per-mask
|
|
333
|
+
const mask = createRouteMask({
|
|
334
|
+
routeTree,
|
|
335
|
+
from: '/photos/$photoId/modal',
|
|
336
|
+
to: '/photos/$photoId',
|
|
337
|
+
params: (prev) => ({ photoId: prev.photoId }),
|
|
338
|
+
unmaskOnReload: true,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
// Per-link
|
|
342
|
+
<Link
|
|
343
|
+
to="/photos/$photoId/modal"
|
|
344
|
+
params={{ photoId }}
|
|
345
|
+
mask={{ to: '/photos/$photoId', params: { photoId } }}
|
|
346
|
+
unmaskOnReload
|
|
347
|
+
>
|
|
348
|
+
Open Photo
|
|
349
|
+
</Link>
|
|
350
|
+
|
|
351
|
+
// Router-wide default
|
|
352
|
+
const router = createRouter({
|
|
353
|
+
routeTree,
|
|
354
|
+
unmaskOnReload: true,
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Common Mistakes
|
|
359
|
+
|
|
360
|
+
### 1. HIGH: Using deprecated `NotFoundRoute`
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
// WRONG — NotFoundRoute blocks notFound() and notFoundComponent from working
|
|
364
|
+
import { NotFoundRoute } from '@tanstack/react-router'
|
|
365
|
+
const notFoundRoute = new NotFoundRoute({ component: () => <p>404</p> })
|
|
366
|
+
const router = createRouter({ routeTree, notFoundRoute })
|
|
367
|
+
|
|
368
|
+
// CORRECT — use notFoundComponent on root route
|
|
369
|
+
export const Route = createRootRoute({
|
|
370
|
+
component: () => <Outlet />,
|
|
371
|
+
notFoundComponent: () => <p>404</p>,
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### 2. MEDIUM: Expecting `useLoaderData` in `notFoundComponent`
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
// WRONG — loader may not have completed
|
|
379
|
+
notFoundComponent: () => {
|
|
380
|
+
const data = Route.useLoaderData() // may be undefined!
|
|
381
|
+
return <p>{data.title} not found</p>
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// CORRECT — use safe hooks
|
|
385
|
+
notFoundComponent: () => {
|
|
386
|
+
const { postId } = Route.useParams()
|
|
387
|
+
return <p>Post {postId} not found</p>
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 3. MEDIUM: Leaf routes cannot handle not-found errors
|
|
392
|
+
|
|
393
|
+
Only routes with children (and therefore an `<Outlet>`) can render `notFoundComponent`. Leaf routes (routes without children) will never catch not-found errors — the error bubbles up to the nearest parent with children.
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
// This route has NO children — notFoundComponent here will not catch
|
|
397
|
+
// unmatched child paths (there are no child paths to unmatch)
|
|
398
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
399
|
+
// notFoundComponent here only works for notFound() thrown in THIS route's loader
|
|
400
|
+
// It does NOT catch path-based not-founds
|
|
401
|
+
notFoundComponent: () => <p>Not found</p>,
|
|
402
|
+
})
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### 4. MEDIUM: Expecting masked URLs to survive sharing
|
|
406
|
+
|
|
407
|
+
Masking data lives in `location.state` (browser history). When a masked URL is copied, shared, or opened in a new tab, the masking data is lost. The browser navigates to the visible (masked) URL directly.
|
|
408
|
+
|
|
409
|
+
### 5. HIGH (cross-skill): Using `reset()` alone instead of `router.invalidate()`
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
// WRONG — reset() clears the error boundary but does NOT re-run the loader
|
|
413
|
+
function ErrorFallback({ error, reset }: { error: Error; reset: () => void }) {
|
|
414
|
+
return <button onClick={reset}>Retry</button>
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// CORRECT — invalidate re-runs loaders and resets the error boundary
|
|
418
|
+
function ErrorFallback({ error }: { error: Error; reset: () => void }) {
|
|
419
|
+
const router = useRouter()
|
|
420
|
+
return (
|
|
421
|
+
<button
|
|
422
|
+
onClick={() => {
|
|
423
|
+
router.invalidate()
|
|
424
|
+
}}
|
|
425
|
+
>
|
|
426
|
+
Retry
|
|
427
|
+
</button>
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Cross-References
|
|
433
|
+
|
|
434
|
+
- **router-core/data-loading** — `notFound()` thrown in loaders interacts with error boundaries and loader data availability. `errorComponent` retry requires `router.invalidate()`.
|
|
435
|
+
- **router-core/type-safety** — `notFoundComponent` data is typed as `unknown`; validate before use.
|