Server Actions: Forms, Mutations, and useActionState
12 min read
Progressive-Enhancement Forms with Server Actions
Server Actions declared with "use server" can be passed directly to a form's action prop. The browser will submit the form even without JavaScript, giving you progressive enhancement for free. When JS is present, Next.js intercepts the submission and calls the action as a function.
tsx
// src/lib/actions/contact.ts
"use server";
import { revalidatePath } from "next/cache";
export type FormState = {
error?: string;
success?: boolean;
};
export async function submitContact(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
if (!name || !email) {
return { error: "Name and email are required." };
}
// Imagine saving to a database here
await saveContact({ name, email });
revalidatePath("/contact");
return { success: true };
}
async function saveContact(data: { name: string; email: string }) {
console.log("Saved:", data);
}useActionState for Pending and Error States
useActionState (React 19 / Next.js 15) is the canonical hook for wiring a Server Action to a Client Component form. It gives you the current state returned by the action and a isPending boolean from useFormStatus.
tsx
"use client";
import { useActionState } from "react";
import { submitContact, type FormState } from "@/lib/actions/contact";
const initialState: FormState = {};
export default function ContactForm() {
const [state, action, isPending] = useActionState(submitContact, initialState);
return (
<form action={action}>
{state.error && <p className="text-red-500">{state.error}</p>}
{state.success && <p className="text-green-600">Message sent!</p>}
<input name="name" placeholder="Your name" required />
<input name="email" type="email" placeholder="Your email" required />
<button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</button>
</form>
);
}Keep Server Actions in a separate file (e.g. src/lib/actions/) rather than inline in components. This makes them reusable and easier to test.
Ready to test yourself?
Practice Next.js— quiz & coding exercises