React 19 Forms (useActionState)
Overview
With the introduction of Server Actions, forms became the focal point of modern React data mutation. However, managing the loading states, error messages, and success confirmations for these Server Actions still required manual boilerplate using useTransition.
React 19 standardizes and drastically simplifies this by introducing form-specific hooks, primarily `useActionState` (formerly experimental useFormState) and `useFormStatus`.
useActionState takes a Server Action and returns the current state of that action (e.g., success messages or Zod validation errors returned from the server).
useFormStatus is a hook designed to be used inside child components of a <form>. It automatically reaches up to the parent form and tells the child if the form is currently submitting, allowing you to easily build a <SubmitButton> that disables itself and shows a spinner without passing any props.
Syntax
// --- actions.js (Runs on Server) ---
"use server";
export async function createUser(previousState, formData) {
const email = formData.get("email");
if (!email.includes("@")) return { error: "Invalid email!" };
await db.insert(email);
return { success: "User created!" };
}
// --- SubmitButton.js (Client Component) ---
"use client";
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
// Automagically knows if the parent form is currently submitting!
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Saving...' : 'Submit'}</button>;
}
// --- Form.js (Client Component) ---
"use client";
import { useActionState } from 'react';
import { createUser } from './actions';
import { SubmitButton } from './SubmitButton';
export default function Form() {
// 1. Hook intercepts the Server Action.
// 'state' holds the return value of createUser (e.g., { error: "..." })
const [state, formAction] = useActionState(createUser, { error: null });
return (
// 2. Pass the hijacked formAction to the form
<form action={formAction}>
<input name="email" />
{state?.error && <p className="text-red-500">{state.error}</p>}
<SubmitButton />
</form>
);
}Common Pitfalls
- Using useFormStatus in the parent: Calling
useFormStatus()in the same component that renders the<form>tag will not work; it will always returnpending: false. The hook ONLY works when called inside a child component that is nested inside the<form>tree. (e.g.,<form><MyButton /></form>).
Interview Questions
useActionState solve when working with Server Actions?When a native <form action={serverAction}> executes, there is no built-in way for the server function to send validation errors or success messages back down to the UI. useActionState bridges this gap. It intercepts the action, tracks its pending state, and captures the exact JSON object returned by the server, exposing it to the UI component for rendering.
useFormStatus useful for building design system components?Before useFormStatus, a generic <SubmitButton> needed an isLoading prop passed down from the parent form. With useFormStatus, the button component can be completely autonomous. You can drop it into any form in the application, and it will automatically disable itself when that specific form is submitting, drastically cleaning up the codebase.
Real-World Example
Optimistic UI Updates (useOptimistic): Optimistic updates are the hallmark of premium, app-like experiences (like iMessage or WhatsApp). When you send a message, it immediately appears on the screen (often slightly grayed out) before the server even receives it. React 19's useOptimistic hook makes this complex pattern trivial to implement.
import { useOptimistic, useActionState } from 'react';
import { addMessageAction } from './actions';
export function ChatThread({ messages }) {
// React 19 provides useOptimistic for immediate UI feedback
const [optimisticMessages, addOptimistic] = useOptimistic(
messages, // Initial state
(state, newMessage) => [...state, { text: newMessage, sending: true }] // Update logic
);
const [state, formAction] = useActionState(async (prevState, formData) => {
// 1. Instantly update the UI so the user thinks it was instantaneous
addOptimistic(formData.get("message"));
// 2. Actually execute the slow Server Action in the background
return await addMessageAction(formData);
}, null);
return (
<div>
{optimisticMessages.map(msg => (
<div className={msg.sending ? 'opacity-50' : 'opacity-100'}>
{msg.text}
</div>
))}
<form action={formAction}>
<input name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}Check Your Knowledge
Test your understanding of React 19 Forms (useActionState) with these quick questions.