Lesson 72 of 80
Two hooks for tracking a form action's pending state and result — without hand-rolling a useState for 'is this submitting right now.'
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
React's form actions (<form action={someFunction}>) let a form submit straight to an async function instead of an onSubmit handler you write yourself. That's convenient, but you still need to know things like "is the submission in flight right now" and "what did the action return." useFormStatus and useActionState are the two hooks built to answer exactly those questions, without a manual useState and try/finally around every submit handler.
They solve different halves of the same problem. useFormStatus reads the pending status of the nearest parent <form> from anywhere underneath it. useActionState wraps an action function itself, and gives you back the action's latest returned state plus a pending flag, no matter where you call it.
useFormStatus — read from a form you don't own directly:
"use client";
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
}
function ProfileForm({ action }) {
return (
<form action={action}>
<input name="name" />
<SubmitButton /> {/* a child of the form, not the form itself */}
</form>
);
}
SubmitButton never receives a pending prop — it reaches up to the surrounding <form> for it. That only works because SubmitButton is inside the <form>, as a separate component. Calling useFormStatus in ProfileForm itself, the component that renders the <form> tag, always returns pending: false — there's no form to be "nearest" to yet at that point in the tree.
useActionState — track the action's own result and pending state, wherever you call it:
import { useActionState } from "react";
async function updateName(prevState, formData) {
const name = formData.get("name");
if (!name) return { error: "Name is required" };
await saveName(name);
return { error: null };
}
function ProfileForm() {
const [state, formAction, isPending] = useActionState(updateName, { error: null });
return (
<form action={formAction}>
<input name="name" />
{state.error && <p>{state.error}</p>}
<button disabled={isPending}>Save</button>
</form>
);
}
useActionState takes your action and an initial state, and returns a wrapped version of the action (formAction) to actually pass to <form action={...}>. Your updateName function's signature changes to accept prevState as its first argument — that's how the hook feeds your previous result back in on the next submission.
useFormStatus only works in a component that's a descendant of the <form>, never the same component that renders the <form> tag. This is the single most common bug — calling it in ProfileForm above instead of SubmitButton, then wondering why pending is always false.useActionState's action function must accept (previousState, formData), not just formData. Forgetting the first parameter and writing async function updateName(formData) silently breaks things — formData inside the function would actually be whatever the previous state was, not the form data.isPending from useActionState already covers what people manually build a useState(false) plus a try/finally for. If you're setting a "submitting" boolean by hand around an action, that's a sign this hook already does it.updateName(prevState, formData) has the identical shape as a useReducer reducer: take the current state and one new piece of input, return the next state. The difference is only where the "input" comes from — a dispatch call in one case, a form submission's FormData in the other. Once you've internalized the reducer lesson's (state, action) => newState pattern, useActionState is that same pattern, not a new one to memorize.