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
Link component - prefetching¶
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
Link component - scrolling behavior¶
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
navigation lifecycle¶
The order of events when a user clicks a Link:
- Prefetch - Link prefetches the target page data (if enabled)
- Click - browser intercepts the click (no full page reload)
- Loading - Next.js shows the loading UI (from
loading.tsx) - Render - Server Component renders , HTML streams to client
- Replace - the page content replaces without layout unmounting
- 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