DEV Community

Cover image for React 19 useFormStatus Returning False? I Built a SubmitButton That Fixes It

React 19 useFormStatus Returning False? I Built a SubmitButton That Fixes It

Shubhra Pokhariya on September 29, 2026

A few weeks back I wrote about the useFormStatus bug that got me the hardest, the one where pending sits at false forever because the hook was call...
Collapse
 
elijahbrown profile image
Elijah Brown •

The aria-disabled pending pattern is a good catch. One related trap on contact forms: a well-formed email on a domain with no MX still looks like a successful submit until the confirmation bounces.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks Elijah, good trap to call out. type="email" only checks the shape, not whether the domain can actually receive mail.

I'd do the DNS check server side inside the action and return an error state from there. One caveat is that a missing MX isn't always a dead end, since mail can fall back to an A record, so I'd warn the user rather than hard-block on that alone.

Collapse
 
elijahbrown profile image
Elijah Brown •

If you end up wanting that as one call rather than wiring the DNS lookups into the action yourself, I built an API for the domain side: MX, Null MX and a list of about 9,000 known throwaway providers. Happy to say more if it helps.

Thread Thread
 
shubhradev profile image
Shubhra Pokhariya •

Thanks for the offer, Elijah. For now, I'm keeping the check inside the action, but I'll keep it in mind.

Thread Thread
 
elijahbrown profile image
Elijah Brown •

Keeping it inside the action makes sense when the check is part of the same decision boundary. The important bit is that the result stays explicit instead of silently changing the submit path.

Collapse
 
elijahbrown profile image
Elijah Brown •

Agreed on warning rather than blocking there, since a domain with no MX falls back to its A record for mail. The one case I'd block outright is a Null MX, because that domain has published that it takes no mail at all.

Collapse
 
usernameinvalid profile image
Larry Z. •

This is the kind of post that actually moves the needle for people shipping real forms.

You didn’t just restate the useFormStatus placement gotcha — you productized the fix and then kept going until the second-order failures showed up. The switch from disabled to aria-disabled + manual click guard is the part that quietly matters most. Focus loss on submit is one of those a11y landmines that only becomes obvious once a keyboard user hits Enter and the focus vanishes into the void. Using aria-disabled and aria-busy while still blocking the action is the correct pattern, and most people still get this wrong.

The intent-aware status hook is equally sharp. Pending is form-scoped, not button-scoped, so the naïve approach lights up every submit button. Reading the submitted intent back out of status.data is the cleanest way I’ve seen to scope the pending state without inventing extra React context. Bonus points for calling out the HTML default-button behavior when someone presses Enter — that’s the kind of detail that saves a future “why does Save Draft keep winning?” debugging session.

PendingFieldset is a nice pragmatic choice. Native <fieldset disabled> is still under-used, and the CSP-aware unstyled escape hatch shows you’ve actually shipped this under real constraints. Same for the live-region status text — aria-busy alone is never enough, and you handled it correctly.

The second-pass placement checks on the fieldset and status components are the real sign of quality. Silent failure is the original bug’s signature; making every consumer of useFormStatus fail loudly in development is the right hardening. The unsafeSkipFormCheck escape hatch for portals is also well-judged — the React tree vs DOM mismatch is exactly the edge case that bites people who are otherwise doing everything right.

Known limits section is honest and useful (per-button formAction, requestSubmit, double-click race). That’s the difference between a clever snippet and something someone can actually drop into a production codebase with eyes open.

Single-file, zero-deps, accessibility-first solution that fixes both the original silent failure and the follow-on UX/a11y issues. This is the version people should be copying.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks Larry 🙂 I really appreciate you taking the time to go through this in such detail. I went with aria-disabled because I didn't want the pending fix to introduce a new focus problem for keyboard users.

For the intent handling, the hook reads the submitted button's name and value from status.data, which lets me narrow the form-level pending state down to the action that was actually submitted.

Collapse
 
webdeveloperhyper profile image
Web Developer Hyper •

Yes, it's hard to notice a silent bug. I wish all bugs were noticeable and easy to find. Nice bug smash! 😄

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks! 😊 The silent ones usually take the longest to notice. No error, no console message, the button just never changes. That's why I added a dev warning for it.

Collapse
 
mudassirworks profile image
Mudassir Khan •

the mental model shift that got us is that useFormStatus reads from the nearest form ancestor in the component tree, not the closest DOM form element. we had a modal with a native form element where status was always false because our SubmitButton was outside the actual React Form component boundary. wrapping with your SubmitButton pattern fixed it immediately. the subtle part is that it also needs to be in a component that renders inside the action bound form, not just a sibling. did you run into cases where the pending state was delayed even with the correct nesting?

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks Mudassir, glad it fixed the modal! 😊 The nesting point is right. The hook has to be called in a component that renders inside the form, so a button rendered outside the form's React tree won't see the form status.

One thing worth checking in that modal: if the form submits through a plain onSubmit that only calls preventDefault(), pending stays false even with correct nesting, because there's no Action for the hook to track. The form needs a function passed to its action.

On the delay, not in my testing so far. I've tested Chromium and Firefox, and the timing edge case I know about is two synchronous clicks in the same task getting through before React re-renders and pending flips.

Collapse
 
kyisaiah47 profile image
kyisaiah47 •

How does the placement check identify an intentional portal without letting unsafeSkipFormCheck hide a component that was accidentally rendered outside the form?

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

It doesn't try to infer whether the portal is intentional. The check only looks at the DOM and warns when it can't find a parent form, while useFormStatus follows the React tree, so a portal and a misplaced component can look the same to it. unsafeSkipFormCheck is an explicit opt-out for when you know the component is meant to sit outside the form.

So yes, setting it can hide an accidental placement. That's why I made it explicit and gave it the unsafe name, rather than making the check silently skippable.

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala •

Shubhra, the keyboard part was really useful 😀 I wouldn’t have thought about focus getting lost when using disabled. And the two buttons showing pending for the same form was another good catch!

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks Hema! 😊 The focus behavior is easy to overlook, especially when disabled seems like the obvious way to prevent another submit. And the two buttons showing the same pending state makes more sense once you remember that pending belongs to the form, not the button you clicked.