Handbooks / Next.js / Chapter 4
Navigation and the Client Router Cache
7 pages · ~4 min✓ Reviewed
Builds on Data Fetching and Caching. Next up: Server Actions and Route Handlers.
Navigation Mental Model
1. Soft vs hard navigation
| Soft | Hard | |
|---|---|---|
| Trigger | <Link>, router.push | <a>, window.location, URL bar, refresh |
| Fetches | RSC payload only | Full document |
| Client state | Kept | Reset |
| Router cache | Kept | Reset |
2. Partial rendering
- Click Link
- Fetch payloadchanged segments
- Layouts staymounted
- Swap segment
- Shared layouts do not re-render
- Only segments below the changed one are fetched and rendered
- Layout state and client React state survive
3. Link and transitions
<Link href>is the primary API for internal routes- It enables prefetching and client transitions
- Navigation is wrapped in a React transition
- Old UI stays visible until the new one is ready or
loading.jsshows
tsx
<Link href="/blog">Blog</Link> router.push('/blog')
both are soft navigations
Plain <a>
An internal <a> reloads the document and wipes client state.
Link Prefetching
1. When It Prefetches
<Link>prefetches when it enters the viewport- Production only: no prefetch in
next dev
| Route | Prefetched |
|---|---|
| Static | the full route payload |
Dynamic + loading.js | shared layout down to the fallback |
Dynamic, no loading.js | almost nothing |
2. Controlling It
| Setting | Effect |
|---|---|
| default | prefetch as above |
prefetch={false} | off |
prefetch={true} | also the full dynamic route, cached longer |
Navigation Hooks
1. useRouter
| Method | Effect |
|---|---|
push(url) | adds history entry |
replace(url) | overwrites entry |
back() forward() | move in history |
refresh() | re-fetch route |
prefetch(url) | preload route |
- Client components only
- Import from
next/navigation
2. Reading the URL
usePathname(): path only, no query, like/blog/1- Use it for active-link styling
useSearchParams(): read-onlyURLSearchParams- Update: build a new string, then push
Client Router Cache and Staleness
1. What It Is
- In-memory, per-session cache of RSC payloads by route segment
- Separate from the server Data Cache and Full Route Cache
- Cleared on a hard refresh
- Each tab has its own cache
2. Staleness Window
| Page | Stale time |
|---|---|
Dynamic (staleTimes.dynamic) | 0s default |
| Static | 5 min |
Dynamic + prefetch={true} | 5 min |
- Tune with
experimental.staleTimesin next.config
javascript
experimental: {
staleTimes: { dynamic: 30, static: 180 },
}seconds, in next.config
3. Fixing Stale UI
redirect, notFound, Scroll and Loading
1. redirect and notFound
redirect(url): 307, but 303 in server actionspermanentRedirect(url): 308- Valid in server components, actions, route handlers
notFound(): nearestnot-found.js, 404- Unmatched URLs fall through to the root
not-found
- Both throw a special error: never inside
try/catch - Call after the try block, or rethrow in the catch
- Client components:
useRouter().push;redirect()is render-only, not event handlers
2. Scroll
- Default: top of the new page, or first segment element if off-screen
<Link scroll={false}>keeps positionrouter.push(url, { scroll: false })keeps position
3. loading.js
Navigation Cheat Sheet
1. Link and useRouter
| Option | Effect |
|---|---|
<Link href> | Soft navigation; prefetches in viewport (production only) |
prefetch={false} | Prefetch off |
scroll={false} | Keeps scroll position |
replace | Swaps the history entry |
useRouterfrom next/navigation:push,replace,back,forward,refresh,prefetch- Client components only
usePathnamegives the path;useSearchParamsgives read-only URLSearchParamsuseSearchParamsneeds a<Suspense>boundary
2. Server Side
Check yourself
A layout holds a client counter at 3. The user clicks a plain <a href="/about">. What does the counter show on the new page, and what if it were <Link>?
- Plain
<a>is a hard navigation: full load, counter resets to 0 <Link>is a soft navigation: layout stays mounted, counter stays 3
Spot the bug: why does a missing post never show the 404 page?
notFound()works by throwing, so thecatchswallows it- Call it outside the try block, or rethrow in the catch
try { const p = await getPost(id) if (!p) notFound() } catch (e) { return <Oops /> }
A route handler adds an item. Going back to a static /items page via <Link> still shows the old list. Why, and what fixes it?
- The client router cache holds the static page for 5 min; only server actions invalidate it
- Call
router.refresh()after the mutation