Skip to content
C.W.K.
Stream
Lesson 03 of 08 · published

useActionState

~22 min · useActionState, form state, errors

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Renamed in React 19

useFormState from react-dom is gone. Use useActionState from react. The new hook also returns a third value, isPending — you no longer need useFormStatus for the common case of disabling the submit button.

Shape

const [state, formAction, isPending] = useActionState(action, initialState); — render state, pass formAction to the form, disable on isPending.

Action signature

The action receives (prevState, formData) and returns the next state. Whatever it returns becomes state on the next render. This is your channel for "the action ran, here's what happened": success messages, validation errors, optimistic markers.

Model action state as a discriminated result: idle, field error, global error, or success. This lets the UI move focus, connect messages to fields, and avoid guessing from text. Keep durable records in the database and use state only to describe the latest submission. Test repeated submissions and slow responses so an older result cannot overwrite newer state. Pending UI is not decoration; it shows which submission currently owns the screen. isPending returning to false does not prove server-rendered data is current. The action must invalidate the relevant reads and the next render must receive them. Duplicating domain data into hook state creates two sources of truth.

Return success, field error, and server error in turn. Submit twice quickly and observe pending behavior. After the action resolves, compare the state message with the newly rendered server value instead of stopping at a disabled button demo.

Code

useActionState in a sign-up form·tsx
'use client';
import { useActionState } from 'react';

async function signUp(prev: any, formData: FormData) {
  'use server';
  const email = formData.get('email') as string;
  if (!email.includes('@')) return { error: 'Invalid email', success: false };

  await db.user.create({ data: { email } });
  return { error: null, success: true };
}

export default function SignUpForm() {
  const [state, formAction, pending] = useActionState(signUp, {
    error: null,
    success: false,
  });

  return (
    <form action={formAction} className="space-y-2">
      <input name="email" type="email" required className="border px-2 py-1" />
      {state.error && <p className="text-red-500 text-sm">{state.error}</p>}
      {state.success && <p className="text-green-600 text-sm">Welcome!</p>}
      <button disabled={pending} className="rounded bg-blue-600 px-3 py-1 text-white">
        {pending ? 'Signing up&hellip;' : 'Sign up'}
      </button>
    </form>
  );
}

External links

Exercise

Convert a form that currently uses useFormState from react-dom to useActionState from react. Show the diff and confirm pending state now comes from the hook itself.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.