TanStack Router 05 ⚛️ Navigation — Using Link components
The previous chapter covered the route tree and its type generation. This chapter covers the component that the user actually clicks to move between routes: the <Link> component. It is the TanStack Router equivalent of React Router’s <Link>, but the type safety is stricter. The to prop is not a generic string. It is a union of the paths defined in the route tree. The params prop is required when the path has a dynamic segment. The search prop is typed against the validateSearch schema. A typo in the path, a missing param, or a malformed search object is a compile-time error, not a runtime failure. The critical detail is that the <Link> component does not need a from prop in most cases because the router infers the current route from the render context.
Key point: The <Link> component is imported from @tanstack/react-router. The to prop is a typed route path from the route tree. The params prop is required for dynamic segments. The search prop is typed against the route’s validateSearch. The activeProps and inactiveProps are the mechanism for the active state styling. The <Link> renders an <a> with a valid href for accessibility.
Why Link components require a specific mental model
The typed path problem. The to prop is not a string. It is a union of the paths in the route tree. The /about path is valid because the about.tsx file exists. The /invalid path is a compile-time error because no route matches it. The /posts/$postId path requires the params prop because the path has a dynamic segment. The type system enforces all of this before the code runs.
The params problem. A dynamic segment is a placeholder. The /posts/$postId path has the $postId segment. The <Link> must provide the params={{ postId: "123" }} to fill the placeholder. Without the params prop, the compile fails. With the wrong type (a number instead of a string), the compile fails. The router does not guess.
The search problem. A route can have search parameters (the query string). The validateSearch function on the route defines the schema. The <Link>‘s search prop is typed against that schema. A missing required search param or a wrong type is a compile error. The search object is not serialized manually; the router handles the URL encoding.
The active state problem. The <Link> has activeProps and inactiveProps that apply when the current route matches the to prop. The activeOptions prop controls the matching: exact: true requires the exact path, and includeSearch includes the search params in the match. The data-status attribute is set to "active" or "inactive" on the rendered element, which allows the CSS styling without custom class logic.
The trade-off. The typed <Link> is stricter than the React Router <Link>, but the strictness is the point. The runtime errors are moved to the compile time. The cost is that the route tree must be generated and registered for the types to work. The benefit is that a broken link never reaches the browser.
a. The basic Link and the typed path
The <Link> is imported from @tanstack/react-router. The to prop is the path.
import { Link } from '@tanstack/react-router'
function NavBar() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/posts">Posts</Link>
</nav>
)
}
The /about path is valid because the about.tsx route exists. The /invalid path is a compile error.
// TypeScript error: '/invalid' is not assignable to valid routes
<Link to="/invalid">Invalid</Link>
The <Link> renders an <a> with a valid href. The click is intercepted, and the router navigates without a full page reload. The right-click and the middle-click work as expected because the href is a real URL.
b. The Link with params and search
The <Link> requires the params prop for the dynamic segments.
<Link to="/posts/$postId" params={{ postId: "123" }}>
Post 123
</Link>
The postId is typed as string by default. If the route uses params.parse to convert the param to a number, the params prop is typed as number.
// With params.parse: (p) => ({ postId: Number(p.postId) })
<Link to="/posts/$postId" params={{ postId: 123 }}>
Post 123
</Link>
The search prop is typed against the validateSearch schema.
// Route definition
export const Route = createFileRoute('/posts')({
validateSearch: (search) => ({ page: Number(search.page) || 1 }),
})
// Link
<Link to="/posts" search={{ page: 2 }}>
Page 2
</Link>
The page is typed as number. A string value is a compile error. The search object is serialized to ?page=2 in the URL.
c. The active state and the activeProps
The <Link> has activeProps and inactiveProps that are applied when the route matches.
<Link
to="/about"
activeProps={{ className: "active" }}
inactiveProps={{ className: "inactive" }}
>
About
</Link>
The activeProps are merged with the element’s props when the route matches. The className is applied. The style object is also supported.
The activeOptions prop controls the matching.
<Link
to="/posts"
activeOptions={{ exact: true, includeSearch: false }}
>
Posts
</Link>
The exact: true requires the path to match exactly. Without it, the /posts link is active for the /posts/123 URL. The includeSearch: true requires the search params to match as well.
The data-status attribute is set on the rendered element. The CSS can target it without the custom class.
a[data-status="active"] {
font-weight: bold;
}
The data-status is "active" when the route matches, "inactive" otherwise. The "loading" value is set during the navigation transition (with the router’s default pending state).
Complete Example Session
# ============================================
# PART 1: BASIC LINK
# ============================================
import { Link } from '@tanstack/react-router'
<Link to="/">Home</Link>
<Link to="/about">About</Link>
# ============================================
# PART 2: TYPED PATH
# ============================================
<Link to="/about">About</Link>
<Link to="/invalid">Invalid</Link>
// TypeScript error: invalid path
# ============================================
# PART 3: DYNAMIC PARAMS
# ============================================
<Link to="/posts/$postId" params={{ postId: "123" }}>
Post 123
</Link>
# ============================================
# PART 4: TYPED PARAMS
# ============================================
// With params.parse: (p) => ({ postId: Number(p.postId) })
<Link to="/posts/$postId" params={{ postId: 123 }}>
Post 123
</Link>
# ============================================
# PART 5: SEARCH PARAMS
# ============================================
<Link to="/posts" search={{ page: 2 }}>
Page 2
</Link>
# ============================================
# PART 6: ACTIVE PROPS
# ============================================
<Link
to="/about"
activeProps={{ className: "active" }}
inactiveProps={{ className: "inactive" }}
>
About
</Link>
# ============================================
# PART 7: EXACT MATCH
# ============================================
<Link to="/posts" activeOptions={{ exact: true }}>
Posts
</Link>
# ============================================
# PART 8: DATA STATUS
# ============================================
a[data-status="active"] { font-weight: bold; }
a[data-status="inactive"] { color: gray; }
# ============================================
# PART 9: VERIFY NAVIGATION
# ============================================
# Click "About" → URL changes to /about → No page reload.
# Click "Post 123" → URL changes to /posts/123 → Param passed.
# Click "Page 2" → URL changes to /posts?page=2 → Search param passed.
The nine parts trace the navigation: basic link, typed path, dynamic params, typed params, search params, active props, exact match, data status, and verification.
Quick Reference
Link Props
| Prop | Purpose |
|---|---|
to | Typed route path |
params | Dynamic segment values |
search | Typed search params |
activeProps | Props when active |
inactiveProps | Props when inactive |
activeOptions | Matching behavior |
hash | URL hash |
state | History state |
activeOptions
| Option | Default | Effect |
|---|---|---|
exact | false | Require exact path match |
includeSearch | false | Include search in match |
Data Attributes
| Attribute | Value |
|---|---|
data-status | "active", "inactive", "loading" |
Other Navigation Components
| Component | Purpose |
|---|---|
<Link> | Typed navigation |
useNavigate | Imperative navigation |
redirect | Loader redirect |
Best Practices
✅ Do This:
<Link to="/posts/$postId" params={{ postId: "123" }}>
<!-- Provide params for dynamic segments --> # ✅
<Link to="/about" activeProps={{ className: "active" }}>
<!-- Use activeProps for the active state --> # ✅
<Link to="/posts" activeOptions={{ exact: true }}>
<!-- Use exact for parent routes --> # ✅
❌ Don’t Do This:
# Use a string variable for the path
<!-- Loss of type safety --> # ❌
# Provide params as a number without parse
<!-- Type error or wrong runtime value --> # ❌
# Use activeOptions exact: false for a root link
<!-- The link is active on every page --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Type error on to | Invalid path | Check the route tree |
| Param missing | Dynamic segment not provided | Add params prop |
| Search type error | Wrong type | Check validateSearch |
| Link always active | No exact | Add activeOptions |
| No active class | No activeProps | Add activeProps |
Real-World Examples
1. Basic Link
<Link to="/about">About</Link>
2. Dynamic Params
<Link to="/posts/$postId" params={{ postId: "1" }}>Post</Link>
3. Search Params
<Link to="/posts" search={{ page: 2 }}>Page 2</Link>
4. Active Props
<Link to="/about" activeProps={{ className: "active" }}>About</Link>
5. Exact Match
<Link to="/posts" activeOptions={{ exact: true }}>Posts</Link>
6. Hash
<Link to="/about" hash="section">Section</Link>
7. State
<Link to="/login" state={{ from: "/dashboard" }}>Login</Link>
8. useNavigate
const navigate = useNavigate()
navigate({ to: "/posts/$postId", params: { postId: "1" } })
9. Redirect
throw redirect({ to: "/login" })
10. Data Status
a[data-status="active"] { font-weight: bold; }
Visual
Link Type Safety
┌──────────────────────────────────────────────┐
│ Route tree │
│ ├── / │
│ ├── /about │
│ └── /posts/$postId │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ <Link to="/about"> │
│ → Valid │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ <Link to="/invalid"> │
│ → TypeScript error │
└──────────────────────────────────────────────┘
Active State
┌──────────────────────────────────────────────┐
│ Current URL: /posts/123 │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ <Link to="/posts" activeOptions={{exact:true}}>│
│ → inactive (path mismatch) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ <Link to="/posts"> │
│ → active (path prefix matches) │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Basic link | <Link to="/path"> |
| Typed path | Union from route tree |
| Dynamic params | params={{ key: value }} |
| Search params | search={{ key: value }} |
| Active props | activeProps, inactiveProps |
| Exact match | activeOptions={{ exact: true }} |
| Data attribute | data-status |
| Imperative | useNavigate |
| Redirect | throw redirect(...) |
Key takeaways:
- The
toprop is a typed path. It is a union of the paths in the route tree. An invalid path is a compile-time error, not a runtime failure. The type safety is the differentiator from React Router. - The
paramsprop is required for dynamic segments. The/posts/$postIdpath requires theparams={{ postId: "123" }}. The type isstringunlessparams.parsetransforms it. - The
searchprop is typed againstvalidateSearch. The route’svalidateSearchschema defines the type. A wrong type or a missing required field is a compile error. - The
activePropsanddata-statusare the active state mechanism. TheactivePropsmerges with the element’s props. Thedata-statusattribute is set to"active","inactive", or"loading"for CSS targeting. - The
activeOptions.exactcontrols the matching precision. Without it, a/postslink is active for the/posts/123URL. With it, the link is active only when the path matches exactly.
Remember: The <Link> is the typed navigation component. The to is the path. The params is the dynamic. The search is the query. The activeProps is the styling. The data-status is the CSS hook. The useNavigate is the imperative. The redirect is the loader. The exam tests the type safety and the active state configuration.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!