Skip to the solution
HardFrontend6 min read

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

1.1k views

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.

  • react
  • react-19
  • hooks
  • forms

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactOne shared SubmitButton in two different forms, plus the silent wrong-place bug
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 74 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

What are Effect Events (`useEffectEvent`) and what problem do they solve?

Continue