Navigation: Link & useNavigate

Lesson 5 — typed routing between your mobile screens

Your capstone tree has screens. Now make them link to each other. TanStack Router's navigation API is the payoff for all that file-based structure: because routes are generated from files, every to and every params value is type-checked at compile time. A typo in a path is a red squiggle, not a 404.

Primary navigation tool. Renders a real <a> tag with a valid href, so cmd/ctrl-click, middle-click, and "open in new tab" all work for free.

import { Link } from '@tanstack/react-router'

// routes/_app/(tabs)/feed/$postId/index.tsx
export default function PostScreen() {
  return (
    <Link to="/profile/u/$username" params={{ username: 'ana' }}>
      View @ana's profile
    </Link>
  )
}
⚡ Why this is the killer feature to="/profile/u/$username" is checked against your generated route tree. Misspell it as /profle/... and TypeScript errors. Pass params={{ user: 'ana' }} (wrong key) and TypeScript errors. The route tree is the source of truth — you can't navigate to a route that doesn't exist.

Type-Safe Params

Because the file is named $username, the generated types know this route needs a username param. The shape of params is derived from the path:

Route pathparams shape required
/feed/$postId{ postId: string }
/profile/u/$username{ username: string }
/posts/$postId/$revisionId{ postId: string, revisionId: string }
/home{} (none)

Forget a param -> compile error. Add a phantom param -> compile error. The link is impossible to write wrong.

useNavigate — imperative navigation

For side-effect navigations: after a form submit, after an async action resolves, after a timeout. Don't reach for it for anything the user clicks — <Link> is better there (it provides href, accessibility, cmd-click).

import { useNavigate } from '@tanstack/react-router'

function NewPostButton() {
  // pass `from` here for max type-safety
  const navigate = useNavigate({ from: '/feed' })

  const handleSubmit = async (e) => {
    e.preventDefault()
    const res = await fetch('/api/posts', { method: 'POST', ... })
    const { id: postId } = await res.json()
    navigate({ to: '/feed/$postId', params: { postId } })
  }

  return <form onSubmit={handleSubmit}>...</form>
}
🧠 Pass from at the hook useNavigate({ from: '/feed' }) anchors the navigation's type inference to your current route. Do it once at the hook, not on every call. Relative paths and param inference both benefit.

The other two flavors (for completeness)

None of these replace server-side redirects. For pre-mount redirects, do them on the server.

Navigation Options

Both <Link> and navigate() accept these modifiers (from the NavigateOptions interface):

OptionEffect
replace: trueReplace current history entry instead of pushing. User can't hit "back" to return.
resetScroll: falseKeep scroll position instead of jumping to top (default true).
viewTransition: trueWrap navigation in document.startViewTransition() — animated transitions.
reloadDocument: trueFull page reload instead of SPA navigation.
ignoreBlocker: trueOverride navigation blockers (e.g. "unsaved changes" prompts).

For mobile apps, replace is common — after login, you don't want "back" returning to the login screen.

Relative Paths: "." and ".."

Like a filesystem. From /posts/$postId:

<Link to=".">Reload current route (re-run loaders)</Link>
<Link to="..">Up to parent (/posts)</Link>
<Link from="/posts" to=".">Same as above, explicit</Link>

"." = same route. ".." = one route up the tree. Useful for "back to list" buttons and loader re-runs.

Active Link Styling

For tab bars and nav menus. The link tracks whether its to matches the current URL:

<Link
  to="/feed"
  activeProps={{ className: 'tab-active' }}
  inactiveProps={{ className: 'tab-inactive' }}
>
  Feed
</Link>

The rendered <a> also gets data-status="active" when matched — useful if you prefer CSS attribute selectors over prop merging. This is exactly how you'd build the bottom tab bar's highlighted state.

Next.js → TanStack Quick Map

Next.jsTanStack
<Link href="/posts/42"><Link to="/posts/$postId" params={{ postId: '42' }}>
router.push('/posts/42')navigate({ to: '/posts/$postId', params: { postId: '42' } })
router.replace('/login')navigate({ to: '/login', replace: true })
router.back()router.history.back()
(no equivalent)<Link to=".."> relative parent

The big shift: Next.js uses interpolated strings (href="/posts/42"); TanStack uses typed route templates + params object. No more string-concatenating your way into 404s.

Practice Exercise

You're in routes/_app/(tabs)/feed/$postId/index.tsx (the post detail screen). Build:

  1. A link to the post author's profile at /profile/u/$username (param value: "ana")
  2. A "Back to feed" link using a relative path
  3. An imperative navigation to /feed/$postId with a new postId after a fake delete+create flow

Sketch the three pieces mentally, then reveal.

Show solution
import { Link, useNavigate } from '@tanstack/react-router'

export default function PostScreen() {
  const navigate = useNavigate({ from: '/feed/$postId' })

  const handleRecreate = async () => {
    const { id: newId } = await fetch('/api/posts', { method: 'POST' }).then(r => r.json())
    navigate({ to: '/feed/$postId', params: { postId: newId } })
  }

  return (
    <>
      {/* 1. Typed link to profile */}
      <Link to="/profile/u/$username" params={{ username: 'ana' }}>
        View @ana
      </Link>

      {/* 2. Relative back to feed list */}
      <Link to="..">Back to feed</Link>

      {/* 3. Imperative nav after async action */}
      <button onClick={handleRecreate}>Recreate post</button>
    </>
  )
}

Why this works:

Quick Quiz

1. Which renders a real <a> with a valid href?

useNavigate only
The Link component
The Navigate tag
<Link> renders a real <a>, so cmd/ctrl-click and "open in new tab" work. useNavigate is for side-effects, <Navigate> renders nothing.

2. to="/profile/u/$username" requires which params?

{ profile: string }
{ username: string }
{ u: string }
The text after each $ is the param name. Only $username here, so { username: string }.

3. When should you prefer useNavigate over <Link>?

For all user clicks
For side-effect flows
For static navigation
useNavigate is for navigations triggered by side-effects (form submit, async result). Anything a user clicks should use <Link> for accessibility and href.

4. What does navigate({ to, replace: true }) do?

Reloads the page
Overwrites history
Blocks navigation
replace: true overwrites the current history entry instead of pushing a new one — user can't hit "back" to return. Common after login.

5. From /posts/$postId, what does <Link to=".."> resolve to?

The root path
The parent /posts
The same post
".." navigates up one route in the tree — from /posts/$postId to its parent /posts.

6. What does activeProps do on a <Link>?

Activates the route
Styles when matched
Preloads the route
activeProps is merged into the rendered element when the link's to matches the current URL — perfect for highlighting the active tab.

7. Misspelling to="/profle/u/$username" yields?

A runtime 404
A compile error
Silent no-op
The to value is checked against the generated route tree at compile time. A typo is a TypeScript error, not a runtime 404.

8. Why pass from to useNavigate at the hook?

It speeds up rendering
Anchors type inference
Enables SSR there
from anchors navigation's type inference to your current route. Relative paths and param checking both rely on it. Pass once, not per call.

9. Next.js router.push('/posts/42') maps to?

navigate({ href: '/posts/42' })
navigate({ to, params })
Link({ path: '/posts/42' })
TanStack uses typed route templates, not interpolated strings: navigate({ to: '/posts/$postId', params: { postId: '42' } }).

10. Which attribute marks an active <Link> for CSS?

data-active="true"
data-status="active"
aria-current="match"
Active links get data-status="active". Use it with [data-status="active"] selectors, or use activeProps for inline prop merging.
← Lesson 4: Capstone Lesson 6: Search Params →
🎓 Stuck on something?
Ask your teacher — when to use replace vs default push, how to wire tab bar active states, or how to handle "back" inside a modal flow.

Primary source: TanStack Router — Navigation (the <Link> component, useNavigate, and NavigateOptions sections). Read after the quiz to deepen.