Full-Stack NextPro· 50 min read

Form Validation and Pending UI

Make Server Action forms production-ready: validate the input, send back clear error messages, and show a "Saving…" state while it runs.

What you will learn

  • Validate form data on the server and return errors
  • Show field errors with useActionState
  • Disable the button while submitting with useFormStatus

A bare Server Action form is not enough

You can wire a form to a Server Action, but a real form needs three more things to be usable: it must reject bad input (an empty title, a too-short comment), tell the user what went wrong, and show that something is happening while it saves so they do not click twice. This lesson adds all three.

Step 1 — validate on the server

Never trust what a form sends — a user can leave fields blank or send junk. So the Server Action checks the data first and, if it is bad, returns an errors object instead of saving. (Many teams use a library called Zod to describe the rules; here we validate by hand so the idea is crystal clear.)

The action validates first and returns an error object if the input is bad
// app/actions.js
'use server';

export async function createComment(prevState, formData) {
  const text = formData.get('text');

  // validate
  if (!text || text.trim().length < 3) {
    return { error: 'Comment must be at least 3 characters.' };
  }

  // ...save the comment to the database...
  return { success: true };
}

Note: Output (return values, not screen text): - Empty or too-short text -> { error: "Comment must be at least 3 characters." } - Valid text -> { success: true } The action never saves bad data. It hands a result back to the page so the page can show the message.

Step 2 — show errors with useActionState

How does that returned { error } get onto the screen? React gives a hook called useActionState. You hand it your action; it gives you back the latest state (whatever the action returned last time) and a wrapped action to put on the form. When the action returns { error: ... }, that becomes the new state and you display it.

Note the action above takes prevState as its first argument — useActionState passes the previous returned state there, and the form data second.

useActionState wires the action to the form and exposes its returned state
'use client';
import { useActionState } from 'react';
import { createComment } from './actions';

export default function CommentForm() {
  const [state, formAction] = useActionState(createComment, null);

  return (
    <form action={formAction}>
      <input name="text" placeholder="Your comment" />
      <button type="submit">Post</button>
      {state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
    </form>
  );
}

Note: Output: - Submit a 1-letter comment -> red text appears: "Comment must be at least 3 characters." - Submit a real comment -> no error; the action saved it. The error came from the server, travelled back as state, and rendered under the input — no page reload.

Step 3 — a pending "Saving…" state with useFormStatus

While the action runs, the button should say "Saving…" and be disabled so the user cannot submit twice. React gives useFormStatus, which reports a pending flag that is true while the form is submitting. It must be read from a component inside the form, so we make a small SubmitButton.

useFormStatus turns the button into a live "Saving…" indicator
'use client';
import { useFormStatus } from 'react-dom';

export default function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Saving…' : 'Post'}
    </button>
  );
}

Note: Output (behaviour): While the Server Action is running, the button reads "Saving…" and is greyed out (disabled). When the action finishes, it returns to "Post". The user gets clear feedback and cannot double-submit.

How the three pieces work together, step by step

Here is the full journey when a user submits the comment form:

  1. The user types a comment and clicks Post. Because the form uses action={formAction}, the data is sent to the server.
  2. Instantly, useFormStatus flips pending to true, so the button shows "Saving…" and is disabled.
  3. On the server, createComment runs and validates the text first.
  4. If the text is too short, the action returns { error: ... }. useActionState stores that as state, the red message appears, and nothing was saved.
  5. If the text is valid, the action saves it and returns { success: true }. (You would also revalidatePath here so the new comment appears.)
  6. Either way, the action finishes, pending goes back to false, and the button returns to "Post".
ToolJobWhere it lives
Server-side checkReject bad input, return an errorInside the Server Action
useActionStateHold and show the returned errorThe form (Client) Component
useFormStatusShow the "Saving…" pending stateA child component inside the form

Progressive enhancement — a bonus

Progressive enhancement means the form still works even if JavaScript has not loaded yet. Because a Server Action form posts to the server the normal HTML way, a user on a slow connection can submit before the page is fully interactive. The pending and error niceties are layered on top once JavaScript loads — but the core form never breaks.

Tip: In real projects, replace the hand-written check with Zod: describe the rules once (e.g. z.string().min(3)) and let it produce the error messages. The wiring with useActionState and useFormStatus stays exactly the same.

Watch out: Always validate on the server, inside the action — never rely only on the browser. A user can bypass browser checks, so the server is the only place that truly protects your database.

Q. Which hook shows a "Saving…" pending state while a Server Action form is submitting?

Answer: useFormStatus reports a pending flag (true while submitting) from inside the form, so you can disable the button and show "Saving…". useActionState handles the returned error/state instead.

✍️ Practice

  1. Make a Server Action that returns { error } when a name field is empty, and show it with useActionState.
  2. Add a SubmitButton using useFormStatus that reads "Saving…" while the action runs.

🏠 Homework

  1. Build a contact form whose Server Action validates name and message, returns field errors via useActionState, and shows a disabled "Sending…" button via useFormStatus.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →