skip to content

A Next.js App Router Server Action updates a row in the database and returns without throwing, but when the user navigates back to /posts they still see the old list. Explain why the write did not change what they see, and what the action must do so the list is fresh.

level: middleimportance: must knowfreq 72%

answer

  1. the database write is invisible to the framework
  2. more than one cache holds that page
  3. invalidation is an explicit call
  4. revalidatePath or revalidateTag, inside the action
  5. takes effect on the next visit

basics

~20 s

The write succeeded but nothing told Next.js its caches were wrong, so the navigation is answered from the client Router Cache and the server Data Cache. Call revalidatePath('/posts') or revalidateTag(...) from 'next/cache' inside the action, after the write.

solid answer

~50 s

Writing to the database is invisible to Next.js. The `/posts` route was rendered and cached earlier: the RSC payload sits in the client-side Router Cache for that session, and the `fetch` results behind it may sit in the server-side Data Cache, with a static route also holding a cached render on the server. A successful Server Action does not touch any of them, so the back-navigation is served from cache and looks unchanged. Invalidation is an explicit step: after the write, call `revalidatePath('/posts')` — or `revalidateTag('posts')` if the reads were tagged — from `next/cache`, inside the action. Those calls mark the cached entries stale and also expire the client Router Cache for that session, so the next visit re-renders and refetches. They do not refetch anything at the moment they are called; the work happens on the next request for that route.

code

typescript · 12 lines
typescript
'use server'
import { revalidatePath } from 'next/cache'

export async function renamePost(id: string, formData: FormData) {
  await fetch(`https://api.example.com/posts/${id}`, {
    method: 'PATCH',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ title: formData.get('title') }),
  })

  revalidatePath('/posts')
}

go deeper

for a junior

Be ready to say plainly that Next.js caches pages and fetch results, and that after a write you must call a revalidation function from 'next/cache' inside the Server Action rather than trusting the database update alone.

for a middle

Explain the mechanics: which cache holds what, that the call marks entries stale rather than refetching, and that the current route re-renders after an action while other routes stay in the client Router Cache.

for a senior

Show diagnosis. Given a stale-after-write report, work out whether the stale bytes came from the client cache, a cached fetch, or a cached static render, and prove it before adding invalidation calls at random.

for a principal

Own the convention: where invalidation lives (co-located with the write, never scattered across UI code), how the team keeps it from being forgotten, and how much staleness the product is actually willing to tolerate per surface.

## The mental model: a write is not a signal Next.js caches what it has *rendered* and what it has *fetched*. Your `UPDATE` statement happens in a database the framework knows nothing about, so nothing about a successful mutation invalidates a cache entry. The App Router's contract is explicit invalidation: you mutate, then you name what is now wrong. ## What is holding the old data After the user has visited `/posts` once, the same bytes can be held in more than one place: - **The client Router Cache.** The browser keeps the RSC payload for routes it has already rendered so that a back-navigation or a `<Link>` click is instant. This is per-session, in memory, and is the usual reason a user sees stale content immediately after their own write. - **The server Data Cache.** If the list page reads with `fetch`, and that fetch is cached, the result is stored server-side across requests. Re-rendering the route does not help here — the render simply reuses the cached response. - **A cached render of a static route.** If `/posts` is statically rendered, the server holds the rendered output and serves it without running your code at all. A weak answer stops at "the browser cached it". The discriminating detail is that even a forced re-render can still produce old data, because the *data* layer is cached independently of the *route* layer. ## What the action should call ```ts 'use server' import { revalidatePath } from 'next/cache' export async function renamePost(id: string, title: string) { await db.post.update({ where: { id }, data: { title } }) revalidatePath('/posts') } ``` `revalidatePath` and `revalidateTag` are imported from `next/cache`. Both mark cached entries as stale and expire the client Router Cache for the current session, so the next navigation to an affected route re-renders on the server and re-runs the uncached reads. The common mistake is expecting them to be imperative refreshes. They are not: `revalidatePath('/posts')` does not go and render `/posts`. It records that the entry is no longer valid, and the actual re-render happens the next time someone asks for that route. Nothing streams down to a user sitting on another page. ## Where the call belongs These are mutation-context APIs. They belong in a Server Action or a Route Handler — somewhere that runs *because* something changed. Calling `revalidatePath` while a Server Component is rendering is an error: rendering is supposed to be a pure read, and a render that invalidates caches would be self-referential. ## The current route versus every other route There is a nuance worth being precise about. When a Server Action runs, Next re-renders the route the user is currently on and sends the fresh RSC payload back on the same response — that is how a form submission updates the page in place without a client refetch. But that re-render is still allowed to reuse the Data Cache. So the current page can update its *rendered* markup and still show pre-mutation *values* if the read was cached. And routes the user is not on are untouched entirely; they sit in the Router Cache until something expires them. Invalidation covers both cases. ## Choosing what to name - If the mutation affects one known route, `revalidatePath('/posts')` is the direct answer. - If the affected data appears on several routes, tag the reads (`fetch(url, { next: { tags: ['posts'] } })`) and call `revalidateTag('posts')` once, so callers do not have to enumerate every page that happens to render a post. ## Client-side refreshes are not a substitute `router.refresh()` from `next/navigation` clears the Router Cache for the current route and asks the server for a fresh render. It is useful, but it is a client-side hammer: it does not invalidate the server Data Cache, so if the stale value came from a cached fetch, the refreshed render happily serves the same stale value again. Reaching for `window.location.reload()` is worse still — it throws away the SPA transition and usually hides the same bug. ## What an interviewer is listening for That you distinguish "the write worked" from "the caches were told"; that you name at least the client Router Cache and the server Data Cache as separate things; that you call the invalidation from inside the action; and that you know it takes effect on the next request rather than immediately.

  • The user is already sitting on /posts when the action runs. Does the list update there even without a revalidation call?
    The markup can update — Next re-renders the current route after the action and returns a fresh RSC payload on the same response. But that re-render may reuse cached `fetch` results, so if the read hit the Data Cache you get a fresh render of stale values. Only invalidation forces the read itself to run again.
  • Where is it legal to call revalidatePath, and what happens if you call it from a Server Component?
    It belongs in mutation contexts: Server Actions and Route Handlers. Calling it during a Server Component render throws — rendering is treated as a pure read, and letting a render invalidate caches would make renders self-invalidating and non-deterministic.
  • Does calling revalidatePath in one user's action fix the stale page for a different user who is already browsing?
    It fixes the server-side caches for everyone, so their next request for that route re-renders with fresh data. It does not push anything to an open tab: that browser's Router Cache entry is only invalidated for the session that ran the action, so another user sees the change on their next navigation to it.

The write updates the warehouse; the caches are printed catalogues already in customers' hands. Nothing reprints a catalogue until you say which one is out of date.

saying these in an interview costs you the question

  • Assumes a successful database write invalidates framework caches
  • Thinks revalidatePath immediately refetches and pushes the page
  • Believes router.refresh() also clears the server Data Cache
  • Fixes stale data with a full window reload
  • Says returning a value from the action refreshes other routes

context