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...
For further actions, you may consider blocking this person and/or reporting abuse
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.
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.
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.
Thanks for the offer, Elijah. For now, I'm keeping the check inside the action, but I'll keep it in mind.
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.
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.
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
disabledtoaria-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. Usingaria-disabledandaria-busywhile 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.datais 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-awareunstyledescape hatch shows you’ve actually shipped this under real constraints. Same for the live-region status text —aria-busyalone 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
useFormStatusfail loudly in development is the right hardening. TheunsafeSkipFormCheckescape 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.
Thanks Larry 🙂 I really appreciate you taking the time to go through this in such detail. I went with
aria-disabledbecause 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.Yes, it's hard to notice a silent bug. I wish all bugs were noticeable and easy to find. Nice bug smash! 😄
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.
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?
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
onSubmitthat only callspreventDefault(),pendingstays false even with correct nesting, because there's no Action for the hook to track. The form needs a function passed to itsaction.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
pendingflips.How does the placement check identify an intentional portal without letting
unsafeSkipFormCheckhide a component that was accidentally rendered outside the form?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
useFormStatusfollows the React tree, so a portal and a misplaced component can look the same to it.unsafeSkipFormCheckis 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
unsafename, rather than making the check silently skippable.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!Thanks Hema! 😊 The focus behavior is easy to overlook, especially when
disabledseems like the obvious way to prevent another submit. And the two buttons showing the same pending state makes more sense once you remember thatpendingbelongs to the form, not the button you clicked.