Skip to content

Next Navigation

The Link component prefetches pages , the router navigates without full page reloads , and every developer eventually clicks something that 404s because they forgot how routing works

Client-side navigation in Next.js is instant because the framework prefetches pages before you click them. But "instant" doesn't mean "free" - every prefetched page consumes bandwidth and memory. Understanding the prefetch behavior means you control what loads and when

The Link component from next/link is your primary navigation tool

import Link from 'next/link'

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/dashboard">Dashboard</Link>
      <Link href="/dashboard/settings">Settings</Link>
    </nav>
  )
}

Link prefetches pages automatically:

  • Static routes - the full page HTML is prefetched and cached
  • Dynamic routes - the data fetching is prefetched (layout + loading boundary)
  • In-viewport links - prefetched when the link enters the viewport
  • Hover links - prefetched on hover (desktop only)

Disable prefetching when you don't need it:

<Link href="/heavy-page" prefetch={false}>
  Heavy page - don't prefetch
</Link>

Use prefetch={false} for pages behind auth , rarely-visited pages , or pages that trigger expensive database queries. That one attribute saves your database from thousands of prefetch queries

By default , navigating with Link scrolls to the top of the page

// prevent scroll to top on navigation
<Link href="/dashboard" scroll={false}>
  Stay scrolled
</Link>

// scroll to specific element
<Link href="/blog#comments">
  Jump to comments
</Link>

useRouter - programmatic navigation

For navigation triggered by events (form submission , login , button clicks) , use useRouter

'use client'

import { useRouter } from 'next/navigation'

export default function LoginForm() {
  const router = useRouter()

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    // ... login logic ...
    router.push('/dashboard') // redirect after login
  }

  return <form onSubmit={handleSubmit}>{/* form fields */}</form>
}

Key difference from next/router (legacy):

API next/navigation next/router
Hook useRouter useRouter
Pathname usePathname() router.pathname
Search params useSearchParams() router.query
Navigation router.push() router.push()
Refresh router.refresh() router.reload()

Always import from next/navigation in App Router projects. next/router still exists for Pages Router backward compatibility. Importing the wrong one silently returns empty objects and wastes hours of debugging

'use client'

import { usePathname, useSearchParams } from 'next/navigation'

export default function ActiveLink({ href, children }: { href: string; children: React.ReactNode }) {
  const pathname = usePathname()
  const isActive = pathname === href

  return (
    <Link href={href} className={isActive ? 'active' : ''}>
      {children}
    </Link>
  )
}

shallow routing

Shallow routing updates the URL without triggering a data fetch on the server

'use client'

import { useRouter, useSearchParams } from 'next/navigation'

export default function SearchFilters() {
  const router = useRouter()
  const searchParams = useSearchParams()

  function updateFilter(key: string, value: string) {
    const params = new URLSearchParams(searchParams.toString())
    params.set(key, value)

    // updates URL without server re-fetch
    router.push(`/products?${params.toString()}`, { scroll: false })
  }

  return (
    <div>
      <button onClick={() => updateFilter('category', 'electronics')}>
        Electronics
      </button>
      <button onClick={() => updateFilter('sort', 'price_asc')}>
        Price: Low to High
      </button>
    </div>
  )
}

The URL updates client-side. The page component reads searchParams and filters data locally. No server request

router.refresh() - the secret weapon

router.refresh() re-renders the current route without a full page reload. It re-fetches data from Server Components while preserving Client Component state

'use client'

import { useRouter } from 'next/navigation'

export default function CreatePostButton() {
  const router = useRouter()

  async function handleCreate() {
    await fetch('/api/posts', { method: 'POST', body: JSON.stringify({ title: 'New' }) })
    router.refresh() // re-fetches server data , keeps client state
  }

  return <button onClick={handleCreate}>Create Post</button>
}

This is the pattern for mutations: call the API , then router.refresh() to get fresh data from the server. No manual state management

The order of events when a user clicks a Link:

  1. Prefetch - Link prefetches the target page data (if enabled)
  2. Click - browser intercepts the click (no full page reload)
  3. Loading - Next.js shows the loading UI (from loading.tsx)
  4. Render - Server Component renders , HTML streams to client
  5. Replace - the page content replaces without layout unmounting
  6. Complete - URL updates , scroll position adjusts

This all happens in the background. The user sees instant navigation with a loading state if the server takes more than a few hundred milliseconds

prerequisites

next_03_routing.md - you should understand dynamic routes , route groups , and parallel routes


next → next_05_rendering.md