Server Actions & Forms
Mutate data without writing API endpoints. Call server code directly from a form or button.
Server actions are async functions that run on the server but can be called from client UI as if they were local. They are the App Router's replacement for hand-rolled API routes when you just need to mutate data.
Declaring one
Put "use server" at the top of the file (for an actions file) or at the top of the function. Server actions must be async.
// app/actions.js
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export async function createTodo(formData) {
const title = formData.get("title");
await db.todo.create({ data: { title } });
revalidatePath("/todos");
}Using one from a form
Pass the action directly to a <form action={...}>. The browser submits the form to the server action and the result is applied.
// app/todos/page.js
import { createTodo } from "@/app/actions";
export default function TodosPage() {
return (
<form action={createTodo}>
<input name="title" required />
<button type="submit">Add</button>
</form>
);
}No useState, no fetch, no event handler. The form works without JavaScript at all — and gets progressively enhanced when JS is available.
Pending state with useFormStatus
For a submit button that shows a spinner during submission, use the useFormStatus hook from react-dom inside a client component:
"use client";
import { useFormStatus } from "react-dom";
export default function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Saving…" : "Save"}
</button>
);
}Returning errors with useActionState
Need to show validation errors or success messages? Pair the action with useActionState:
"use client";
import { useActionState } from "react";
import { createTodo } from "./actions";
export default function NewTodo() {
const [state, formAction] = useActionState(createTodo, { error: null });
return (
<form action={formAction}>
<input name="title" />
<button type="submit">Add</button>
{state.error && <p>{state.error}</p>}
</form>
);
}The action receives the previous state as its first argument and returns the new state.
revalidatePath or revalidateTag from next/cache to refresh the relevant route's cache.Try it
Update this server action to invalidate the /posts route's cache after a post is created.
Need a hint?
Import revalidatePath from next/cache and call it with the path to refresh.
Quiz
Pick the best answer. You only get one shot per question.
1. What does `"use server"` mark?
2. After mutating data in a server action, how do you make the relevant page show fresh data?
3. Which hook gives you the pending state of the form submit?