Forms are one of the most common places Server Actions get used in practice. This lesson covers building a complete form with pending states, validation error handling, and progressive enhancement.
A Complete Form Backed by a Server Action
Connecting a <form> directly to a Server Action via its action prop gives you a fully working submission flow with zero client-side JavaScript required for the basic case — the browser's native form submission behavior handles it, and Next.js progressively enhances it with a client-side transition when JavaScript is available.
For real applications, you also want validation error messages and a visible pending state while the action runs — React provides useActionState and useFormStatus specifically for this.
// app/actions.ts
"use server";
export async function createPost(prevState: unknown, formData: FormData) {
const title = formData.get("title") as string;
if (!title || title.length < 3) {
return { error: "Title must be at least 3 characters." };
}
await db.post.create({ data: { title } });
return { error: null };
}
Returning an object instead of throwing lets the form display validation errors without a full page reload.
useActionState(action, initialState) wraps a Server Action to track its returned state across submissions.
The wrapped action receives the previous state as its first argument, and FormData as its second.
useFormStatus() (used inside a form's child component) reports whether the enclosing form is currently submitting.
Both hooks require the enclosing component to be a Client Component.
Forms Cheat Sheet
Hooks and patterns for building robust forms.
Hook / Pattern
Purpose
<form action={serverAction}>
Basic, progressively-enhanced submission
useActionState
Track validation errors / returned state across submissions
useFormStatus
Show a pending/loading state during submission
formData.get("field")
Read a specific field's submitted value
Server-side validation
Always re-validate on the server, even with client checks
Showing a Pending State with useFormStatus
useFormStatus must be called from a component rendered inside the <form>, not the form's own component, since it reads status from the nearest parent form context.
SubmitButton must be rendered as a child of the <form> using the Server Action.
Why This Works Without JavaScript
Because <form action={serverAction}> uses the browser's native form submission mechanism under the hood, the form still works — with a full page navigation instead of a smooth client-side transition — even if JavaScript fails to load. This is a meaningful accessibility and resilience improvement over purely client-side form handling.
Common Mistakes
Only validating on the client and skipping server-side validation inside the Server Action itself.
Calling useFormStatus() in the same component that renders the <form>, instead of a child component.
Forgetting useActionState's action receives (previousState, formData), not just formData.
Not disabling the submit button while pending is true, allowing duplicate submissions.
Key Takeaways
Server Actions connect directly to <form action={...}> for progressively-enhanced submissions.
useActionState tracks validation errors and other returned state across submissions.
useFormStatus shows a pending state, but only from a component nested inside the form.
Always validate submitted data on the server, regardless of any client-side checks.
Form submissions still work even without JavaScript, thanks to native form semantics.
Pro Tip
Design your Server Action to always return a consistent shape (like { error: string | null }) even on success, rather than sometimes returning undefined — this makes the client-side state handling in useActionState predictable and avoids extra if checks scattered through your form component.
You've completed the API and backend section. Next, step back and look at Next.js rendering strategies as a whole, tying together everything you've learned so far.