A React 19 form gets an isPending flag from useActionState and its submit button reads pending from useFormStatus. What does each of those flags actually track, and which one belongs in a shared SubmitButton component used across many forms?
answer
- one is scoped to an action, one to a form
- where you can read it decides it
- props versus ambient
- a design-system button knows no parent
- the form owner cannot read its own status
basics
~20 suseActionState's isPending tracks that one specific action and is readable only where the hook is called. useFormStatus().pending tracks the enclosing form's submission and is readable from any descendant, so a shared SubmitButton reads that one and needs no props.
solid answer
~50 sThey differ in what they observe and where they can be read. `useActionState`'s `isPending` is tied to the one action you passed that hook: it is true while that action runs, and it is a plain value in the component that called the hook, so anything else needs it passed down as a prop. `useFormStatus().pending` is tied to a DOM `<form>`: it reports whether *that form* is currently submitting, and any component rendered inside the form can read it without props. For a `SubmitButton` reused across many forms, `useFormStatus` is the right choice — it makes the button self-sufficient, so every form gets a correct disabled and busy state for free instead of each one remembering to thread a boolean down. Keep `isPending` for things the form's owner renders itself: a banner, a disabled fieldset, gating a navigation.
code
jsx · 20 linesimport { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
function SubmitButton({ children }) {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>{pending ? 'Saving…' : children}</button>;
}
export function Settings({ save }) {
const [state, formAction, isPending] = useActionState(save, { error: null });
return (
<form action={formAction}>
<fieldset disabled={isPending}>
<input name="displayName" />
<SubmitButton>Save</SubmitButton>
</fieldset>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}go deeper
Know that both flags say "a submission is in progress", and that a button rendered inside the form can get one from useFormStatus() without receiving any prop.
Explain the scopes: isPending belongs to the specific action passed to useActionState and is a local value, while pending describes the nearest ancestor <form> and is readable by any descendant.
Show the design call: reusable leaf components read the form status so they work in any parent, while the form's owner uses its own isPending for surrounding UI; and know that neither sees async work started outside a form submission.
Be ready to set the convention across a design system: an ambient read makes the button drop-in but hides a structural requirement in no prop, so decide whether that implicit contract is documented, linted, or replaced by an explicit prop for library components.
## Two flags, two scopes Both are `true` during roughly the same window in the simple case, which is why candidates treat them as interchangeable. They are not: they answer different questions and are readable in different places. **`useActionState`'s `isPending` answers: is *this action* running?** It is the third element of the hook's return value, an ordinary boolean living in the component that called the hook. It knows nothing about forms; it knows about the function you handed the hook. **`useFormStatus().pending` answers: is *this form* submitting?** It reads the nearest ancestor `<form>`, so it is only meaningful — and only readable — inside a component rendered beneath that form. ```jsx import { useActionState } from 'react'; import { useFormStatus } from 'react-dom'; function SubmitButton({ children }) { const { pending } = useFormStatus(); // this form's submission return <button type="submit" disabled={pending}>{children}</button>; } function Settings() { const [state, formAction, isPending] = useActionState(save, { error: null }); return ( <form action={formAction}> <fieldset disabled={isPending}> {/* this action */} <input name="name" /> <SubmitButton>Save</SubmitButton> </fieldset> {state.error && <p role="alert">{state.error}</p>} </form> ); } ``` ## Why the shared button uses useFormStatus A reusable button that took `pending` as a prop would work, but the cost is paid at every call site: every form must obtain a flag and remember to pass it, and every new form is a chance to forget. With `useFormStatus` the button is context-aware by construction. Drop it in any form and it behaves correctly, including in forms that have no `useActionState` at all — a plain `<form action={fn}>` still produces a submission status. That is the reusability argument, and it comes with a contract: the button now only works when rendered inside a `<form>`. That dependency is invisible in its props, so a component library documents it, and reviewers learn to spot a `SubmitButton` used outside a form, where `pending` will simply never become true. ## What each one cannot see The distinction becomes concrete at the edges: - **Work not started by a form submit.** If a button's `onClick` kicks off an async operation, no form submission is in flight, so `useFormStatus().pending` stays `false`. A pending flag that must cover that case has to come from somewhere else. - **The component that renders the form.** `useFormStatus()` called there is always `false`, because that component is the form's parent rather than its descendant. `isPending` from `useActionState` has no such restriction — it is available exactly where the hook was called, which is usually that same component. The two are complementary for this reason: one covers the owner, the other covers the subtree. - **Several actions in one component.** Two `useActionState` calls give you two independent `isPending` flags, one per action, and you can render distinct feedback for each. A single form's status is one boolean for the whole form; if a form has two submit buttons pointing at different `formAction` values, every descendant sees the same `pending`, so use `data` or `action` from the status object if you need to tell which submission is running. ## The design rule A useful default: - **Owner-rendered UI → `isPending`.** Disabling a whole `<fieldset>`, showing an error banner, blocking navigation, deciding what to render alongside the form. The owner already has the flag; passing it around inside its own JSX costs nothing. - **Reusable leaf UI → `useFormStatus`.** Submit buttons, spinners, "saving…" labels, anything that ships in a design system and must work in an unknown parent. Both are derived by React from the action's actual lifetime. Neither is a boolean you set — and that is the real point of the pair. The pre-React-19 alternative was `const [isSubmitting, setIsSubmitting] = useState(false)` with a `try`/`finally`, which breaks on early returns, on thrown errors, and on a second submission arriving while the first is still in flight. Replacing that hand-managed flag is the reason both APIs exist, so an answer that reaches for `useState` to "unify" them has missed the point. ## A note on overlap When a form's `action` is the action returned by `useActionState`, both flags are true for the same window: React is running one action, and that action is the form's submission. So a component can legitimately read `isPending` at the top and let a nested button read `pending` — they will agree. Choose per call site by *where the reader lives*, not by which is "more correct".
- A form has two submit buttons with different formAction values. Can a descendant tell which submission is running?Not from `pending` alone — every descendant of that form sees the same boolean. The status object's `action` field holds the function or URL the current submission targets, and `data` holds the submitted `FormData`, so a status-aware component can compare against a known action reference to decide what to display. Two separate `useActionState` calls give you two independent flags instead.
- When would you still pass a pending flag down as a prop rather than reading useFormStatus?When the child is not inside a form, or the pending work is not a form submission — an async operation started from an `onClick`, for example. Also when a component must reflect an action's state while rendered elsewhere in the tree, such as a header spinner. Reading the enclosing form's status only works for descendants of a submitting form.
- What does a component library owe its users when a SubmitButton reads useFormStatus internally?A documented contract that it must be rendered inside a `<form>` with a function action, because the dependency is invisible in its props and the failure mode is silent — the button just never disables. Practically that means saying so in the docs, and considering a development-time warning or a lint convention so misuse is caught rather than shipped as a subtly broken busy state.
saying these in an interview costs you the question
- Says the two flags are interchangeable everywhere
- Passes isPending into a shared button as a prop by default
- Expects useFormStatus to cover async work from an onClick
- Reads useFormStatus in the component that renders the form
- Adds a useState isSubmitting flag alongside either hook