Skip to solution
hardFrontend

What is `useFormStatus` in React 19, and why does it exist?

1.1k views
01

Understand the problem

Question presented to candidate: "Why does useFormStatus only work in a child of the form, and not in the component that renders it?"

What a strong answer should cover:

  • It reads the submission status of the nearest form above it — returning { pending, data, method, action } — without being passed anything.
  • The "must be a child" rule is not arbitrary: it reads a context the <form> element provides, and a component does not sit inside the context it renders. Calling it beside the form returns pending: false forever, silently.
  • It exists to solve prop drilling for a design-system component. A shared <SubmitButton> can show a spinner in any form without every form having to pass a pending prop.
  • It comes from react-dom, not react, because it is DOM-specific — it is about a form element in the DOM.
  • data is the submitted FormData, which lets the button show what is being saved.
  • It is read-only: it reports status, it does not start or control the submission.
  • Compare with useActionState, which gives you the pending flag for an action you defined, in the component that defined it.

Clarifying questions expected:

  • "Is the button a shared component, or local to this one form?" — local buttons can just take a prop.
  • "Do we already have useActionState here?" — then isPending may already be in scope.

Code / implementation expected: Optional. A <SubmitButton> used inside two different forms makes the point immediately.

reactreact-19hooksforms
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes Actions. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The measurem

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

One shared SubmitButton in two different forms, plus the silent wrong-place bug
Run Playground
import { useFormStatus } from "react-dom";
import { useState } from "react";

const wait = (ms) => new Promise((r) => setTimeout(r, ms));

// ✅ Knows nothing about any particular form. Drop it in anywhere.
function SubmitButton({ children }) {
  const { pending, data } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Saving " + (data ? JSON.stringify(data.get("value")) : "") + "…" : children}
    </button>
  );
}

// ❌ THE BUG: this component RENDERS the form, so it is above it, not inside
//    it. pending is false forever — no error, no warning, no spinner.
function BrokenStatus() {
  const { pending } = useFormStatus();
  return (
    <span style={{ fontSize: 12, color: pending ? "#a33" : "#999", marginLeft: 8 }}>
      (from the form's own component: pending={String(pending)})
    </span>
  );
}

function DemoForm({ title, ms }) {
  const [saved, setSaved] = useState(null);
  return (
    <form
      action={async (formData) => {
        await wait(ms);
        setSaved(formData.get("value"));
      }}
      style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 12 }}
    >
      <strong style={{ fontSize: 13 }}>{title}</strong>
      <BrokenStatus />
      <input name="value" defaultValue={title.toLowerCase()} style={{ width: "100%", margin: "6px 0" }} />
      <SubmitButton>Save</SubmitButton>
      {saved && <div style={{ fontSize: 13, color: "#161", marginTop: 6 }}>✓ saved {JSON.stringify(saved)}</div>}
    </form>
  );
}

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <DemoForm title="Profile" ms={900} />
      <DemoForm title="Billing" ms={1600} />

      <p style={{ fontSize: 13, color: "#666" }}>
        Submit either form. The same <code>SubmitButton</code> component reports
        the right form and even names the value being saved — nothing was passed
        to it. Meanwhile the grey text, which calls the same hook one level too
        high, stays <code>false</code> throughout. That silent failure is the
        thing to remember.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 74 of 119 decoded in the React.js track. One more won't hurt.

Back to track