@tanstack/router-core 1.167.2 → 1.167.4
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/dist/cjs/load-matches.cjs +4 -1
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/router.cjs +2 -1
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/esm/load-matches.js +4 -1
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/router.js +2 -1
- package/dist/esm/router.js.map +1 -1
- package/package.json +9 -2
- 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
- package/src/load-matches.ts +4 -1
- package/src/router.ts +2 -1
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: router-core/navigation
|
|
3
|
+
description: >-
|
|
4
|
+
Link component, useNavigate, Navigate component, router.navigate,
|
|
5
|
+
ToOptions/NavigateOptions/LinkOptions, from/to relative navigation,
|
|
6
|
+
activeOptions/activeProps, preloading (intent/viewport/render),
|
|
7
|
+
preloadDelay, navigation blocking (useBlocker, Block), createLink,
|
|
8
|
+
linkOptions helper, scroll restoration, MatchRoute.
|
|
9
|
+
type: sub-skill
|
|
10
|
+
library: tanstack-router
|
|
11
|
+
library_version: '1.166.2'
|
|
12
|
+
requires:
|
|
13
|
+
- router-core
|
|
14
|
+
sources:
|
|
15
|
+
- TanStack/router:docs/router/guide/navigation.md
|
|
16
|
+
- TanStack/router:docs/router/guide/preloading.md
|
|
17
|
+
- TanStack/router:docs/router/guide/navigation-blocking.md
|
|
18
|
+
- TanStack/router:docs/router/guide/link-options.md
|
|
19
|
+
- TanStack/router:docs/router/guide/custom-link.md
|
|
20
|
+
- TanStack/router:docs/router/guide/scroll-restoration.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Navigation
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
Basic type-safe `Link` with `to` and `params`:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Link } from '@tanstack/react-router'
|
|
31
|
+
|
|
32
|
+
function PostLink({ postId }: { postId: string }) {
|
|
33
|
+
return (
|
|
34
|
+
<Link to="/posts/$postId" params={{ postId }}>
|
|
35
|
+
View Post
|
|
36
|
+
</Link>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Core Patterns
|
|
42
|
+
|
|
43
|
+
### Link with Active States
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { Link } from '@tanstack/react-router'
|
|
47
|
+
|
|
48
|
+
function NavLink() {
|
|
49
|
+
return (
|
|
50
|
+
<Link
|
|
51
|
+
to="/posts"
|
|
52
|
+
activeProps={{ className: 'font-bold' }}
|
|
53
|
+
inactiveProps={{ className: 'text-gray-500' }}
|
|
54
|
+
activeOptions={{ exact: true }}
|
|
55
|
+
>
|
|
56
|
+
Posts
|
|
57
|
+
</Link>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `data-status` attribute is also set to `"active"` on active links for CSS-based styling.
|
|
63
|
+
|
|
64
|
+
`activeOptions` controls matching behavior:
|
|
65
|
+
|
|
66
|
+
- `exact` (default `false`) — when `true`, only matches the exact path (not children)
|
|
67
|
+
- `includeHash` (default `false`) — include hash in active matching
|
|
68
|
+
- `includeSearch` (default `true`) — include search params in active matching
|
|
69
|
+
|
|
70
|
+
Children can receive `isActive` as a render function:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<Link to="/posts">
|
|
74
|
+
{({ isActive }) => <span className={isActive ? 'font-bold' : ''}>Posts</span>}
|
|
75
|
+
</Link>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Relative Navigation with `from`
|
|
79
|
+
|
|
80
|
+
Without `from`, navigation resolves from root `/`. To use relative paths like `..`, provide `from`:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { createFileRoute, Link } from '@tanstack/react-router'
|
|
84
|
+
|
|
85
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
86
|
+
component: PostComponent,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
function PostComponent() {
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
{/* Relative to current route */}
|
|
93
|
+
<Link from={Route.fullPath} to="..">
|
|
94
|
+
Back to Posts
|
|
95
|
+
</Link>
|
|
96
|
+
|
|
97
|
+
{/* "." reloads the current route */}
|
|
98
|
+
<Link from={Route.fullPath} to=".">
|
|
99
|
+
Reload
|
|
100
|
+
</Link>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### useNavigate for Programmatic Navigation
|
|
107
|
+
|
|
108
|
+
Use `useNavigate` only for side-effect-driven navigation (e.g., after a form submission). For anything the user clicks, prefer `Link`.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
112
|
+
|
|
113
|
+
function CreatePostForm() {
|
|
114
|
+
const navigate = useNavigate({ from: '/posts' })
|
|
115
|
+
|
|
116
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
117
|
+
e.preventDefault()
|
|
118
|
+
const response = await fetch('/api/posts', { method: 'POST', body: '...' })
|
|
119
|
+
const { id: postId } = await response.json()
|
|
120
|
+
|
|
121
|
+
if (response.ok) {
|
|
122
|
+
navigate({ to: '/posts/$postId', params: { postId } })
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return <form onSubmit={handleSubmit}>{/* ... */}</form>
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The `Navigate` component performs an immediate client-side navigation on mount:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { Navigate } from '@tanstack/react-router'
|
|
134
|
+
|
|
135
|
+
function LegacyRedirect() {
|
|
136
|
+
return <Navigate to="/posts/$postId" params={{ postId: 'my-first-post' }} />
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`router.navigate` is available anywhere you have the router instance, including outside of React.
|
|
141
|
+
|
|
142
|
+
### Preloading
|
|
143
|
+
|
|
144
|
+
Strategies: `intent` (hover/touchstart), `viewport` (intersection observer), `render` (on mount).
|
|
145
|
+
|
|
146
|
+
Set globally:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { createRouter } from '@tanstack/react-router'
|
|
150
|
+
|
|
151
|
+
const router = createRouter({
|
|
152
|
+
routeTree,
|
|
153
|
+
defaultPreload: 'intent',
|
|
154
|
+
defaultPreloadDelay: 50, // ms, default is 50
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Or per-link:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
<Link
|
|
162
|
+
to="/posts/$postId"
|
|
163
|
+
params={{ postId }}
|
|
164
|
+
preload="intent"
|
|
165
|
+
preloadDelay={100}
|
|
166
|
+
>
|
|
167
|
+
View Post
|
|
168
|
+
</Link>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Preloaded data stays fresh for 30 seconds by default (`defaultPreloadStaleTime: 30_000`). During that window it won't be refetched. When using an external cache like TanStack Query, set `defaultPreloadStaleTime: 0` to let the external library control freshness.
|
|
172
|
+
|
|
173
|
+
Manual preloading via the router instance:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import { useRouter } from '@tanstack/react-router'
|
|
177
|
+
|
|
178
|
+
function Component() {
|
|
179
|
+
const router = useRouter()
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
router.preloadRoute({ to: '/posts/$postId', params: { postId: '1' } })
|
|
183
|
+
}, [router])
|
|
184
|
+
|
|
185
|
+
return <div />
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Navigation Blocking
|
|
190
|
+
|
|
191
|
+
Use `useBlocker` to prevent navigation when a form has unsaved changes:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { useBlocker } from '@tanstack/react-router'
|
|
195
|
+
import { useState } from 'react'
|
|
196
|
+
|
|
197
|
+
function EditForm() {
|
|
198
|
+
const [formIsDirty, setFormIsDirty] = useState(false)
|
|
199
|
+
|
|
200
|
+
useBlocker({
|
|
201
|
+
shouldBlockFn: () => {
|
|
202
|
+
if (!formIsDirty) return false
|
|
203
|
+
const shouldLeave = confirm('Are you sure you want to leave?')
|
|
204
|
+
return !shouldLeave
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
return <form>{/* ... */}</form>
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
With custom UI using `withResolver`:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import { useBlocker } from '@tanstack/react-router'
|
|
216
|
+
import { useState } from 'react'
|
|
217
|
+
|
|
218
|
+
function EditForm() {
|
|
219
|
+
const [formIsDirty, setFormIsDirty] = useState(false)
|
|
220
|
+
|
|
221
|
+
const { proceed, reset, status } = useBlocker({
|
|
222
|
+
shouldBlockFn: () => formIsDirty,
|
|
223
|
+
withResolver: true,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<>
|
|
228
|
+
<form>{/* ... */}</form>
|
|
229
|
+
{status === 'blocked' && (
|
|
230
|
+
<div>
|
|
231
|
+
<p>Are you sure you want to leave?</p>
|
|
232
|
+
<button onClick={proceed}>Yes</button>
|
|
233
|
+
<button onClick={reset}>No</button>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Control `beforeunload` separately:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
useBlocker({
|
|
245
|
+
shouldBlockFn: () => formIsDirty,
|
|
246
|
+
enableBeforeUnload: formIsDirty,
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### linkOptions for Reusable Navigation Options
|
|
251
|
+
|
|
252
|
+
`linkOptions` provides eager type-checking on navigation options objects, so errors surface at definition, not at spread-site:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
import {
|
|
256
|
+
linkOptions,
|
|
257
|
+
Link,
|
|
258
|
+
useNavigate,
|
|
259
|
+
redirect,
|
|
260
|
+
} from '@tanstack/react-router'
|
|
261
|
+
|
|
262
|
+
const dashboardLinkOptions = linkOptions({
|
|
263
|
+
to: '/dashboard',
|
|
264
|
+
search: { search: '' },
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Use anywhere: Link, navigate, redirect
|
|
268
|
+
function Nav() {
|
|
269
|
+
const navigate = useNavigate()
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div>
|
|
273
|
+
<Link {...dashboardLinkOptions}>Dashboard</Link>
|
|
274
|
+
<button onClick={() => navigate(dashboardLinkOptions)}>Go</button>
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Also works in an array for navigation bars
|
|
280
|
+
const navOptions = linkOptions([
|
|
281
|
+
{ to: '/dashboard', label: 'Summary', activeOptions: { exact: true } },
|
|
282
|
+
{ to: '/dashboard/invoices', label: 'Invoices' },
|
|
283
|
+
{ to: '/dashboard/users', label: 'Users' },
|
|
284
|
+
])
|
|
285
|
+
|
|
286
|
+
function NavBar() {
|
|
287
|
+
return (
|
|
288
|
+
<nav>
|
|
289
|
+
{navOptions.map((option) => (
|
|
290
|
+
<Link
|
|
291
|
+
{...option}
|
|
292
|
+
key={option.to}
|
|
293
|
+
activeProps={{ className: 'font-bold' }}
|
|
294
|
+
>
|
|
295
|
+
{option.label}
|
|
296
|
+
</Link>
|
|
297
|
+
))}
|
|
298
|
+
</nav>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### createLink for Custom Components
|
|
304
|
+
|
|
305
|
+
Wraps any component with TanStack Router's type-safe navigation:
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
import * as React from 'react'
|
|
309
|
+
import { createLink, LinkComponent } from '@tanstack/react-router'
|
|
310
|
+
|
|
311
|
+
interface BasicLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {}
|
|
312
|
+
|
|
313
|
+
const BasicLinkComponent = React.forwardRef<HTMLAnchorElement, BasicLinkProps>(
|
|
314
|
+
(props, ref) => {
|
|
315
|
+
return <a ref={ref} {...props} className="block px-3 py-2 text-blue-700" />
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
const CreatedLinkComponent = createLink(BasicLinkComponent)
|
|
320
|
+
|
|
321
|
+
export const CustomLink: LinkComponent<typeof BasicLinkComponent> = (props) => {
|
|
322
|
+
return <CreatedLinkComponent preload="intent" {...props} />
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Usage retains full type safety:
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
<CustomLink to="/dashboard/invoices/$invoiceId" params={{ invoiceId: 0 }} />
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Scroll Restoration
|
|
333
|
+
|
|
334
|
+
Enable globally on the router:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
const router = createRouter({
|
|
338
|
+
routeTree,
|
|
339
|
+
scrollRestoration: true,
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
For nested scrollable areas:
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
const router = createRouter({
|
|
347
|
+
routeTree,
|
|
348
|
+
scrollRestoration: true,
|
|
349
|
+
scrollToTopSelectors: ['#main-scrollable-area'],
|
|
350
|
+
})
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Custom cache keys:
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
const router = createRouter({
|
|
357
|
+
routeTree,
|
|
358
|
+
scrollRestoration: true,
|
|
359
|
+
getScrollRestorationKey: (location) => location.pathname,
|
|
360
|
+
})
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Prevent scroll reset for a specific navigation:
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
<Link to="/posts" resetScroll={false}>
|
|
367
|
+
Posts
|
|
368
|
+
</Link>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### MatchRoute for Pending UI
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
import { Link, MatchRoute } from '@tanstack/react-router'
|
|
375
|
+
|
|
376
|
+
function Nav() {
|
|
377
|
+
return (
|
|
378
|
+
<Link to="/users">
|
|
379
|
+
Users
|
|
380
|
+
<MatchRoute to="/users" pending>
|
|
381
|
+
<Spinner />
|
|
382
|
+
</MatchRoute>
|
|
383
|
+
</Link>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Common Mistakes
|
|
389
|
+
|
|
390
|
+
### CRITICAL: Interpolating params into the `to` string
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
// WRONG — breaks type safety and param encoding
|
|
394
|
+
<Link to={`/posts/${postId}`}>Post</Link>
|
|
395
|
+
|
|
396
|
+
// CORRECT — use the params option
|
|
397
|
+
<Link to="/posts/$postId" params={{ postId }}>Post</Link>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Dynamic segments are declared with `$` in the route path. Always pass them via `params`. This applies to `Link`, `useNavigate`, `Navigate`, and `router.navigate`.
|
|
401
|
+
|
|
402
|
+
### MEDIUM: Using useNavigate for clickable elements
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
// WRONG — no href, no cmd+click, no preloading, no accessibility
|
|
406
|
+
function BadNav() {
|
|
407
|
+
const navigate = useNavigate()
|
|
408
|
+
return <button onClick={() => navigate({ to: '/posts' })}>Posts</button>
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// CORRECT — real <a> tag with href, accessible, preloadable
|
|
412
|
+
function GoodNav() {
|
|
413
|
+
return <Link to="/posts">Posts</Link>
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Use `useNavigate` only for programmatic side-effect navigation (after form submit, async action, etc).
|
|
418
|
+
|
|
419
|
+
### HIGH: Not providing `from` for relative navigation
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
// WRONG — without from, ".." resolves from root
|
|
423
|
+
<Link to="..">Back</Link>
|
|
424
|
+
|
|
425
|
+
// CORRECT — provide from for relative resolution
|
|
426
|
+
<Link from={Route.fullPath} to="..">Back</Link>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Without `from`, only absolute paths are autocompleted and type-safe. Relative paths like `..` resolve from root instead of the current route.
|
|
430
|
+
|
|
431
|
+
### HIGH: Using search as object instead of function loses existing params
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
// WRONG — replaces ALL search params with just { page: 2 }
|
|
435
|
+
<Link to="." search={{ page: 2 }}>Page 2</Link>
|
|
436
|
+
|
|
437
|
+
// CORRECT — preserves existing search params, updates page
|
|
438
|
+
<Link to="." search={(prev) => ({ ...prev, page: 2 })}>Page 2</Link>
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
When you pass `search` as a plain object, it replaces all search params. Use the function form to spread previous params and selectively update.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Cross-References
|
|
446
|
+
|
|
447
|
+
- See also: **router-core/search-params/SKILL.md** — Link `search` prop interacts with search param validation
|
|
448
|
+
- See also: **router-core/type-safety/SKILL.md** — `from` narrowing improves type inference on Link
|