The stale-list bug
Here is a classic trap. The user publishes a post, lands back on /blog, and the new post is missing. The cause comes straight from Unit 4: the blog page's data was cached. The database changed but the cache never heard about it.
Server actions fix this by telling Next.js what they touched:
"use server"; import { revalidatePath } from "next/cache"; export async function createPost(formData) { await db.posts.insert({ title: formData.get("title") }); revalidatePath("/blog"); }
revalidatePath("/blog") throws away the cached data for that route, so the next render fetches fresh data and the new post appears. Mutation plus revalidation is the complete pattern: write, then invalidate what you wrote to.
Diagnosing a comment that will not disappear
A delete-comment server action removes a row from the database, but the comments page keeps showing the deleted comment until the cache expires on its own. The missing line is a revalidation call, revalidatePath("/comments"), or a tag-based equivalent.
The database changed and the cached page data did not, because nothing connected the two. Calling revalidatePath for the affected route, or revalidateTag for tagged data, invalidates the stale copy so the next render reflects the deletion.
Deletions make this bug especially visible. With a create, users see something missing and might blame themselves for a failed submit. With a delete, they see something they explicitly removed still sitting on the page, which reads as a broken app.
Sending the user somewhere new
Revalidation refreshes data, but often the user should move too. After publishing, authors expect to land on their new post rather than stay on a blank form. Server actions can end with a navigation by calling redirect:
"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; export async function createPost(formData) { const post = await db.posts.insert({ title: formData.get("title") }); revalidatePath("/blog"); redirect(`/blog/${post.slug}`); }
Decode it:
redirect()throws internally to stop the action, so it must come last. Code after it never runs, and wrapping it in atry/catchout of habit will swallow the navigation.- Order matters: revalidate first, then redirect, so the page the user lands on, and the list they go back to, are both already fresh.
- This is the same "POST, then redirect" pattern you met in Backend with Node.js. It prevents a browser refresh from resubmitting the form.
Getting the order wrong
Picture a createPost action that inserts the row, calls `redirect(/blog/${slug}), and then calls revalidatePath("/blog") on the next line. The revalidation **never runs**, so /blog` can keep serving the stale cached list.
redirect() works by throwing a special control-flow error that ends the action immediately. Anything written after it is dead code, and unlike a normal bug it fails silently: the navigation succeeds, the user lands on their new post, and only the list page is quietly wrong.
The fix is to put cleanup work before the redirect. Write, invalidate, then navigate. That order guarantees the user lands on fresh data and that going back shows a fresh list too.
Showing 'Saving…'
Users need feedback while the action runs. React's useActionState hook (a client-component tool, as you'd expect from the React course) returns a pending flag:
"use client"; import { useActionState } from "react"; import { createPost } from "../actions"; export function PublishForm() { const [state, action, pending] = useActionState(createPost, null); return ( <form action={action}> <input name="title" /> <button disabled={pending}>{pending ? "Saving…" : "Publish"}</button> </form> ); }
While the server action is in flight, pending is true, so the button disables itself and changes label. Same hooks philosophy as always: state in, UI out.
The complete mutation pattern
The full recipe in Next.js has two steps that must both happen. Write to the data source, then call revalidatePath, or revalidateTag when you tag your fetches, so cached pages that display that data are refreshed.
A mutation changes the database, not the cache. The two are separate systems, and nothing informs one about the other automatically. Unless the action explicitly invalidates the affected route or tag, users keep seeing the stale cached copy until it naturally expires.
Why it works out that way
- Both functions come from
next/cache, which is the module that owns cache lifetime in Next.js. - The call belongs right after the write, as in the
db.posts.insertexample above, and before anyredirect. - Choose between them by scope.
revalidatePathtargets a route you can name, whilerevalidateTagtargets every fetch you labelled with a tag, which is handier when the same data feeds several pages.