Trust is earned, not given

A different perspective

2025-04-22 · Projects

React.js, part 13: useFormStatus and pending states — a submit button that cannot double-submit

Part 13from the React.js series · 20 parts in all

Double submissions are the canonical web bug: a slow request, an impatient click, two orders. The traditional defenses — a disabled flag set in one handler and forgotten in another, a debounce, a spinner you have to remember to start — all depend on discipline. useFormStatus removes the discipline requirement by making pending state a property of the form itself.

Reading the enclosing form's status

The hook reads the status of the parent form, so the component must be rendered inside that form. That is the whole trick: the button does not receive a prop, it asks the form it lives in:

'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton({ children }) {
  // Reads the nearest <form>'s pending state - no props, no context needed.
  const { pending, data, method, action } = useFormStatus();
  return <button type="submit" disabled={pending}>{pending ? 'Working…' : children}</button>;
}

export function OrderForm({ placeOrder }) {
  return (
    <form action={placeOrder}>
      <input name="sku" required />
      <SubmitButton>Place order</SubmitButton>
    </form>
  );
}

Because the button is disabled the instant the form goes pending, and re-enabled only when the action settles, there is no window for a second click. The same hook lets you show a progress row (data is the FormData being submitted) or swap the button label without threading state down.

Pending state is not just a boolean

Three distinct states deserve three distinct UIs, and a single pending flag conflates them:

  1. Idle — nothing in flight.
  2. Pending — the action is running; disable and relabel.
  3. Optimistic — the result is already shown (part 11) and pending merely styles it as provisional.

Where it fits with useActionState

useActionState gives you the pending flag for that form's action; useFormStatus gives it to a descendant without prop-drilling. Use the latter when the submit control is a reusable component living several layers down — a design system button you cannot hand a boolean to from every form. Next: refs as props and ref cleanup, the end of forwardRef boilerplate.