The stale-list bug
Classic trap: the user publishes a post, lands back on /blog, and the new post is missing. Why? Unit 4: the blog page's data was cached. The database changed but the cache did not hear 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.
Quiz
A delete-comment server action removes a row from the database, but the comments page keeps showing the deleted comment until the cache expires. What one call is missing from the action?
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, not 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 try/catch by 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) is 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.
Quiz
A createPost action inserts the row, calls redirect(`/blog/${slug}`), and then calls revalidatePath("/blog") on the next line. What goes wrong?
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.
Problem
Fill the blank. The complete mutation pattern in Next.js is: write to the data source, then call __________ so cached pages that display that data are refreshed. (function name)