TL;DR
- Server Actions = async functions that run on server, called from client.
- Mark with
"use server". Can mutate data, revalidate cache, redirect.
- Works with native
<form action={...}> — no API routes needed for mutations.
Basic Server Action
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const body = formData.get("body") as string;
await db.post.create({ data: { title, body } });
revalidatePath("/posts");
redirect("/posts");
}
import { createPost } from "./actions";
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="body" required />
<button type="submit">Create</button>
</form>
);
}
With useActionState (pending/error)
"use client";
import { useActionState } from "react";
import { createPost } from "./actions";
export function PostForm() {
const [state, action, pending] = useActionState(createPost, null);
return (
<form action={action}>
<input name="title" disabled={pending} />
<button disabled={pending}>
{pending ? "Creating..." : "Create"}
</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
Quick Rules
| Do |
Don't |
| Use for mutations (create/update/delete) |
Use for reads (use Server Components) |
| Validate input on server |
Trust client-side validation alone |
revalidatePath/revalidateTag after mutation |
Forget to invalidate cache |
Return { error: "msg" } for validation |
Throw errors (shows error.tsx) |