| | |

TanStack Router 05 ⚛️ Navigation — Using Link components

⏱️ Estimated read: 9 min | |

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

PropPurpose
toTyped route path
paramsDynamic segment values
searchTyped search params
activePropsProps when active
inactivePropsProps when inactive
activeOptionsMatching behavior
hashURL hash
stateHistory state

activeOptions

OptionDefaultEffect
exactfalseRequire exact path match
includeSearchfalseInclude search in match

Data Attributes

AttributeValue
data-status"active", "inactive", "loading"

Other Navigation Components

ComponentPurpose
<Link>Typed navigation
useNavigateImperative navigation
redirectLoader 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

PitfallWhy It HappensFix
Type error on toInvalid pathCheck the route tree
Param missingDynamic segment not providedAdd params prop
Search type errorWrong typeCheck validateSearch
Link always activeNo exactAdd activeOptions
No active classNo activePropsAdd 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

ItemValue
Basic link<Link to="/path">
Typed pathUnion from route tree
Dynamic paramsparams={{ key: value }}
Search paramssearch={{ key: value }}
Active propsactiveProps, inactiveProps
Exact matchactiveOptions={{ exact: true }}
Data attributedata-status
ImperativeuseNavigate
Redirectthrow redirect(...)

Key takeaways:

  • The to prop 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 params prop is required for dynamic segments. The /posts/$postId path requires the params={{ postId: "123" }}. The type is string unless params.parse transforms it.
  • The search prop is typed against validateSearch. The route’s validateSearch schema defines the type. A wrong type or a missing required field is a compile error.
  • The activeProps and data-status are the active state mechanism. The activeProps merges with the element’s props. The data-status attribute is set to "active", "inactive", or "loading" for CSS targeting.
  • The activeOptions.exact controls the matching precision. Without it, a /posts link is active for the /posts/123 URL. 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.

Was this helpful?


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!