CPCODELAB
Next.js

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