| name | router-core/navigation |
| description | Link component, useNavigate, Navigate component, router.navigate, ToOptions/NavigateOptions/LinkOptions, from/to relative navigation, activeOptions/activeProps, preloading (intent/viewport/render), preloadDelay, navigation blocking (useBlocker, Block), createLink, linkOptions helper, scroll restoration, MatchRoute. |
| type | sub-skill |
| library | tanstack-router |
| library_version | 1.166.2 |
| requires | ["router-core"] |
| sources | ["TanStack/router:docs/router/guide/navigation.md","TanStack/router:docs/router/guide/preloading.md","TanStack/router:docs/router/guide/navigation-blocking.md","TanStack/router:docs/router/guide/link-options.md","TanStack/router:docs/router/guide/custom-link.md","TanStack/router:docs/router/guide/scroll-restoration.md"] |
Navigation
Setup
Basic type-safe Link with to and params:
import { Link } from "@tanstack/react-router"
function PostLink({ postId }: { postId: string }) {
return (
<Link to="/posts/$postId" params={{ postId }}>
View Post
</Link>
)
}
Core Patterns
Link with Active States
import { Link } from "@tanstack/react-router"
function NavLink() {
return (
<Link
to="/posts"
activeProps={{ className: "font-bold" }}
inactiveProps={{ className: "text-gray-500" }}
activeOptions={{ exact: true }}
>
Posts
</Link>
)
}
The data-status attribute is also set to "active" on active links for CSS-based styling.
activeOptions controls matching behavior:
exact (default false) — when true, only matches the exact path (not children)
includeHash (default false) — include hash in active matching
includeSearch (default true) — include search params in active matching
Children can receive isActive as a render function:
<Link to="/posts">
{({ isActive }) => <span className={isActive ? "font-bold" : ""}>Posts</span>}
</Link>
Relative Navigation with from
Without from, navigation resolves from root /. To use relative paths like .., provide from:
import { createFileRoute, Link } from "@tanstack/react-router"
export const Route = createFileRoute("/posts/$postId")({
component: PostComponent,
})
function PostComponent() {
return (
<div>
{/* Relative to current route */}
<Link from={Route.fullPath} to="..">
Back to Posts
</Link>
{/* "." reloads the current route */}
<Link from={Route.fullPath} to=".">
Reload
</Link>
</div>
)
}
useNavigate for Programmatic Navigation
Use useNavigate only for side-effect-driven navigation (e.g., after a form submission). For anything the user clicks, prefer Link.
import { useNavigate } from "@tanstack/react-router"
function CreatePostForm() {
const navigate = useNavigate({ from: "/posts" })
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const response = await fetch("/api/posts", { method: "POST", body: "..." })
const { id: postId } = await response.json()
if (response.ok) {
navigate({ to: "/posts/$postId", params: { postId } })
}
}
return <form onSubmit={handleSubmit}>{/* ... */}</form>
}
The Navigate component performs an immediate client-side navigation on mount:
import { Navigate } from "@tanstack/react-router"
function LegacyRedirect() {
return <Navigate to="/posts/$postId" params={{ postId: "my-first-post" }} />
}
router.navigate is available anywhere you have the router instance, including outside of React.
Preloading
Strategies: intent (hover/touchstart), viewport (intersection observer), render (on mount).
Set globally:
import { createRouter } from "@tanstack/react-router"
const router = createRouter({
routeTree,
defaultPreload: "intent",
defaultPreloadDelay: 50,
})
Or per-link:
<Link
to="/posts/$postId"
params={{ postId }}
preload="intent"
preloadDelay={100}
>
View Post
</Link>
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.
Manual preloading via the router instance:
import { useRouter } from "@tanstack/react-router"
function Component() {
const router = useRouter()
useEffect(() => {
router.preloadRoute({ to: "/posts/$postId", params: { postId: "1" } })
}, [router])
return <div />
}
Navigation Blocking
Use useBlocker to prevent navigation when a form has unsaved changes:
import { useBlocker } from "@tanstack/react-router"
import { useState } from "react"
function EditForm() {
const [formIsDirty, setFormIsDirty] = useState(false)
useBlocker({
shouldBlockFn: () => {
if (!formIsDirty) return false
const shouldLeave = confirm("Are you sure you want to leave?")
return !shouldLeave
},
})
return <form>{/* ... */}</form>
}
With custom UI using withResolver:
import { useBlocker } from "@tanstack/react-router"
import { useState } from "react"
function EditForm() {
const [formIsDirty, setFormIsDirty] = useState(false)
const { proceed, reset, status } = useBlocker({
shouldBlockFn: () => formIsDirty,
withResolver: true,
})
return (
<>
<form>{/* ... */}</form>
{status === "blocked" && (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={proceed}>Yes</button>
<button onClick={reset}>No</button>
</div>
)}
</>
)
}
Control beforeunload separately:
useBlocker({
shouldBlockFn: () => formIsDirty,
enableBeforeUnload: formIsDirty,
})
linkOptions for Reusable Navigation Options
linkOptions provides eager type-checking on navigation options objects, so errors surface at definition, not at spread-site:
import {
linkOptions,
Link,
useNavigate,
redirect,
} from "@tanstack/react-router"
const dashboardLinkOptions = linkOptions({
to: "/dashboard",
search: { search: "" },
})
function Nav() {
const navigate = useNavigate()
return (
<div>
<Link {...dashboardLinkOptions}>Dashboard</Link>
<button onClick={() => navigate(dashboardLinkOptions)}>Go</button>
</div>
)
}
const navOptions = linkOptions([
{ to: "/dashboard", label: "Summary", activeOptions: { exact: true } },
{ to: "/dashboard/invoices", label: "Invoices" },
{ to: "/dashboard/users", label: "Users" },
])
function NavBar() {
(
)
}
createLink for Custom Components
Wraps any component with TanStack Router's type-safe navigation:
import * as React from "react"
import { createLink, LinkComponent } from "@tanstack/react-router"
interface BasicLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {}
const BasicLinkComponent = React.forwardRef<HTMLAnchorElement, BasicLinkProps>(
(props, ref) => {
return <a ref={ref} {...props} className="block px-3 py-2 text-blue-700" />
},
)
const CreatedLinkComponent = createLink(BasicLinkComponent)
export const CustomLink: LinkComponent<typeof BasicLinkComponent> = (props) => {
return <CreatedLinkComponent preload="intent" {...props} />
}
Usage retains full type safety:
<CustomLink to="/dashboard/invoices/$invoiceId" params={{ invoiceId: 0 }} />
Scroll Restoration
Enable globally on the router:
const router = createRouter({
routeTree,
scrollRestoration: true,
})
For nested scrollable areas:
const router = createRouter({
routeTree,
scrollRestoration: true,
scrollToTopSelectors: ["#main-scrollable-area"],
})
Custom cache keys:
const router = createRouter({
routeTree,
scrollRestoration: true,
getScrollRestorationKey: (location) => location.pathname,
})
Prevent scroll reset for a specific navigation:
<Link to="/posts" resetScroll={false}>
Posts
</Link>
MatchRoute for Pending UI
import { Link, MatchRoute } from "@tanstack/react-router"
function Nav() {
return (
<Link to="/users">
Users
<MatchRoute to="/users" pending>
<Spinner />
</MatchRoute>
</Link>
)
}
Common Mistakes
CRITICAL: Interpolating params into the to string
<Link to={`/posts/${postId}`}>Post</Link>
<Link to="/posts/$postId" params={{ postId }}>Post</Link>
Dynamic segments are declared with $ in the route path. Always pass them via params. This applies to Link, useNavigate, Navigate, and router.navigate.
MEDIUM: Using useNavigate for clickable elements
function BadNav() {
const navigate = useNavigate()
return <button onClick={() => navigate({ to: "/posts" })}>Posts</button>
}
function GoodNav() {
return <Link to="/posts">Posts</Link>
}
Use useNavigate only for programmatic side-effect navigation (after form submit, async action, etc).
HIGH: Not providing from for relative navigation
<Link to="..">Back</Link>
<Link from={Route.fullPath} to="..">Back</Link>
Without from, only absolute paths are autocompleted and type-safe. Relative paths like .. resolve from root instead of the current route.
HIGH: Using search as object instead of function loses existing params
<Link to="." search={{ page: 2 }}>Page 2</Link>
<Link to="." search={(prev) => ({ ...prev, page: 2 })}>Page 2</Link>
When you pass search as a plain object, it replaces all search params. Use the function form to spread previous params and selectively update.
Cross-References
- See also: router-core/search-params/SKILL.md — Link
search prop interacts with search param validation
- See also: router-core/type-safety/SKILL.md —
from narrowing improves type inference on Link