Home/Learn/Next.js/Navigation — Link, useRouter and the Router Cache

Navigation — Link, useRouter and the Router Cache

Intermediate
Foundations

Link prefetches on hover and swaps only the part of the tree that changed. The surprises come from the client-side cache sitting between you and fresh data.

Overview

Navigation in the App Router is not a page load and not quite a client-side route change either — Next fetches the new segment's rendered output from the server and swaps it into the existing tree, keeping layouts mounted. That makes it fast and mostly invisible. What is not invisible is the router cache: navigate away and back within a short window and you get the previous render, not a fresh one. Almost every "my page shows old data after I saved" report is that cache rather than a bug in the fetch.

Link and useRouter

Client navigation, and the programmatic equivalent.

next/navigation, and prefetch on view
import Link from 'next/link'

<Link href="/problems">Problems</Link>
<Link href={`/problems/${slug}`} prefetch={false}>Open</Link>
<Link href="/dashboard" replace>Go</Link>          // no history entry

// Link prefetches when it enters the viewport (production only), so
// the destination is often already loaded before the click. That is
// most of why App Router navigation feels instant.

// Programmatic — note the import path
'use client'
import { useRouter } from 'next/navigation'        // NOT next/router
const router = useRouter()
router.push('/problems/two-sum')
router.replace('/login')
router.back()
router.refresh()          // re-fetch the current route from the server

// Reading the current location, all from next/navigation:
const pathname = usePathname()                     // '/problems/two-sum'
const params = useParams()                         // { slug: 'two-sum' }
const search = useSearchParams()                   // read-only

// useSearchParams makes a client component dynamic, so wrap it in a
// Suspense boundary or the whole route opts out of static rendering.

The Router Cache

The client-side cache that makes navigation instant and data look stale.

Stale after save is almost always this
// Next keeps the rendered output of visited routes in memory:
//   Next 14:  30s for dynamic routes, 5 minutes for static ones
//   Next 15:  0 by default for dynamic — the change that fixed most
//             of the "stale after save" complaints

// So: save a problem -> navigate to the list -> see the old list.
// The list did not refetch; it came out of the router cache.

// Three ways to clear it, in order of preference:
router.refresh()                          // re-fetch the current route
revalidatePath('/problems')               // from a server action
// and hard navigation (a full page load) clears everything

// After a mutation, the server action should invalidate rather than
// the client guessing:
'use server'
export async function createProblem(data) {
  await db.problem.create({ data })
  revalidatePath('/problems')             // both caches, server-side
}

// Tune it explicitly if you need to:
// next.config.js
experimental: { staleTimes: { dynamic: 0, static: 180 } }

// Debugging rule of thumb: if a hard refresh shows fresh data but
// in-app navigation does not, it is the router cache.

Search Params and Scroll

Filters in the URL without a full navigation, and the scroll behaviour people trip on.

replace + scroll: false for filters
'use client'
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()

function setFilter(key, value) {
  const next = new URLSearchParams(searchParams)
  value ? next.set(key, value) : next.delete(key)
  next.delete('page')                              // reset paging
  router.replace(`${pathname}?${next}`, { scroll: false })
}
// replace, not push: ten filter tweaks should not be ten back presses.
// scroll: false, or every filter change jumps the user to the top.

// A server component reading the same params re-renders with the new
// data automatically — the URL is the state, and both sides read it.

// nuqs is worth knowing: it gives useState-like ergonomics over
// search params, with types and without the boilerplate above.

// Scroll on normal navigation restores position on back/forward and
// goes to the top on a new push, which is what users expect. Override
// only for in-page filtering.

// Anchors under a sticky header:
html { scroll-padding-top: 5rem }

Key Points to Remember

  • 1Link prefetches in the viewport and swaps only the changed segment, keeping layouts mounted
  • 2Import useRouter from next/navigation — next/router is the Pages Router and will not work
  • 3The client router cache is the usual cause of stale data after a save; router.refresh or revalidatePath clears it
  • 4Next 15 sets the dynamic router cache to 0 by default, which removed most staleness complaints
  • 5Update filters with router.replace and scroll: false so the page neither stacks history nor jumps to the top

Interview Questions

Sign in to ask Aria
1

Why might a list still show old data after a successful save?

Hard
2

What is the difference between router.push and router.refresh?

Medium
3

Why should filter changes use replace rather than push?

Medium

Ask Aria about Navigation — Link, useRouter and the Router Cache

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…