Forms & Data Mutations
Build a complete, validated form with pending and error states using Server Actions and the useActionState hook.
Server-Side Validation
Since a Server Action always runs on the server, it's the right place to validate input — never trust that data reaching it is well-formed, even if you also validate in the browser for a snappier UX.
'use server';
export async function createPost(prevState, formData) { const title = formData.get('title');
if (!title || title.length < 3) { return { error: 'Title must be at least 3 characters.' }; }
await db.post.create({ data: { title } }); revalidatePath('/blog'); return { error: null };}Showing a Pending State with useFormStatus
The useFormStatus hook, used inside a component nested in the form, reports whether that form is currently submitting — perfect for disabling a submit button or showing a spinner.
'use client';import { useFormStatus } from 'react-dom';
function SubmitButton() { const { pending } = useFormStatus(); return ( <button type="submit" disabled={pending}> {pending ? 'Saving…' : 'Save'} </button> );}Returning Errors with useActionState
useActionState wraps a Server Action so the form can hold onto whatever the action returns — like a validation error — and re-render with it, without any manual state management on your part.
'use client';import { useActionState } from 'react';import { createPost } from '../actions';
export default function NewPostForm() { const [state, formAction] = useActionState(createPost, { error: null });
return ( <form action={formAction}> <input name="title" placeholder="Post title" /> {state.error && <p className="error">{state.error}</p>} <SubmitButton /> </form> );}A Complete Example
Put together, a Server Action plus useActionState plus useFormStatus gives you server-validated, progressively-enhanced forms with proper pending and error UI — all without a separate client-side state management layer for the form itself.
Common Beginner Mistakes
Client-side validation is for UX; a Server Action must independently validate, since it can be called directly, bypassing any client-side checks.
It only reports status for the nearest parent <form> — it must be called from a component nested inside the form, not the form's own component.
FAQs
It's not required, but it's common and recommended for anything beyond trivial checks — it keeps validation rules declarative and reusable between client and server.
Yes — the returned state object can carry any shape of data the action returns, including a success flag or a redirect instruction.
Summary
Server Actions combined with useFormStatus and useActionState give you complete, validated forms without a separate client-state library. Next, you'll learn about Middleware — code that runs before a request even reaches your routes.