Part 1 was about waiting well. Part 2 was about not waiting at all. This one is about a value I needed in a component that never created it, and about the ten minutes I lost putting the hook in the one place it can't work.
If you've read Part 1 on useActionState and Part 2 on useOptimistic, both of those hooks answer a question from inside the component that called them. That was never a problem for a two-field newsletter form. It became one the day I split an order form into separate pieces, payment details in one file, the place-order button in another, and both of them needed to know the same thing: is this thing submitting right now.
Nothing about this is broken. It works. But isPending lives in OrderForm, and both children only exist to receive it and hand it to a disabled attribute. Add a shipping section, an order-summary sidebar, a coupon field that should lock during submission, and every one of them needs the same prop threaded through, whether or not anything else about them changes. I'd read that useFormStatus was built to skip exactly this, so I went looking for where to put it.
My first instinct was to call the hook right where isPending used to live, in OrderForm itself, treating it like a drop-in replacement.
I clicked "Place order." Nothing disabled. No console warning, no red error overlay, just a button sitting there fully clickable while the network tab showed a request in flight. My first guess was that placeOrder wasn't actually async, or that I'd broken something in the action itself. I spent a few minutes staring at the action function before I even looked back at the hook.
The actual answer is simpler and, once you see it, a little obvious. useFormStatus doesn't read state that OrderForm owns. It reads the status of the parent , and at the moment that hook call runs, OrderForm is still deciding what to render. The tag doesn't exist as anyone's parent yet, it's the thing being built. There's no ancestor for the hook to find, so it falls back to its default, unpending shape, and it stays that way no matter how long the real submission takes.
The rule underneath that: the component calling useFormStatus has to be a descendant of the form it wants to read, never the component rendering the form itself. Fixing it meant pulling the hook out of OrderForm and putting it somewhere further down the tree.
Same markup, same behavior at a glance, and suddenly pending flips correctly. Neither child receives anything from OrderForm anymore. Both just ask their parent form what's going on and get told the truth.
One detail that's easy to miss on the way there: this hook comes from react-dom, not react. useActionState and useOptimistic both live in react, so if you're moving through this series in order, typing import { useFormStatus } from "react" is a genuinely easy slip, and it fails in a way that doesn't tell you what went wrong either.
Once pending worked, I assumed that was the whole hook. It isn't. Calling it gives you an object with three more properties riding along, and I only found a real use for them once I stopped treating useFormStatus as a fancy boolean.
data is the FormData currently being submitted, available only while something is in flight. I used it to make the button say something more specific than a generic spinner label:
data is null the rest of the time, so that .get("email") call only runs safely because it's tucked inside the pending branch of the ternary. Pull it out and call it unconditionally, and the component throws the first time it renders idle.
method is where I got tripped up briefly, because I assumed it would mirror whatever method attribute I'd have put on the form. It doesn't work that way once you're using a function-based action. Any form using the action prop with a function, which is the entire reason this hook has something to track in the first place, submits as 'post' regardless of anything else you configure. You'd only see 'get' here on a form using a plain URL string as its action, or no action prop at all, and the docs only commit to one thing about that case: action itself comes back null. They stay quiet on pending and data, and since only function-based actions actually run through React's Transition tracking, that's not a combination I'd build anything around.
action is a reference back to whatever function you passed to the form's action prop. It's null for a form with no function-based action, and it's also null if there's no parent form at all, which means a falsy value here doesn't automatically tell you which of those two is true. I hit this once trying to conditionally render something based on whether action existed, and had to remind myself both cases collapse to the same falsy check.
The nicest surprise came later, almost by accident. PlaceOrderButton wasn't just used on the checkout page. It got reused, unmodified, on a "save draft order" form elsewhere in the app. I expected to need some kind of context or ID to tell it which form it belonged to. I didn't. useFormStatus resolves the nearest ancestor form independently everywhere it's rendered, so the same component read the correct pending state on both forms without a single line of extra wiring. That's not an edge case worth working around, it's a real reason to build small, reusable status-aware components in the first place.
The edge case that did surprise me went the other direction. Somewhere in an admin panel, a designer had wired a button outside its form using the HTML form="orderForm" attribute instead of physically nesting it. Pure HTML, no React opinion needed, and it submits the form fine. useFormStatus inside that button read as permanently not-pending, even while the form it was technically attached to was mid-submission. The hook walks the component tree, not the DOM-level form attribute association, so as far as it's concerned, that button was never a child of anything. Worth knowing before it surprises you in a codebase that uses this attribute anywhere.
Not every form needed this. On a two-field signup form with a single button in the same component, useActionState's own isPending, read directly where the hook was called, did the job without adding a second hook and a second rule to remember. useFormStatus earns its place once a form's parts genuinely live in separate components, not before.
And it only ever tells you about a submission already underway. useFormStatus isn't a validation API, it tells you what the form is doing once submission has started, not whether that submission should have been allowed to start in the first place. That decision still belongs upstream, in the action function or wherever else you're handling the submit.
