Community Question Bundle
The React Form Validation Traps I Keep Stepping On
Hand-rolled forms in React look easy until they meet real users. These 5 traps are the ones I keep regressing on: the empty-input case, controlled vs uncontrolled, when to stop hand-rolling, async race conditions, and the accessibility bit nobody catches in review.
The React Form Validation Traps I Keep Stepping On
Hand-rolled forms in React look easy until they meet real users. These 5 traps are the ones I keep regressing on: the empty-input case, controlled vs uncontrolled, when to stop hand-rolling, async race conditions, and the accessibility bit nobody catches in review.
By @zurihayes
April 4, 2026
·
Updated May 18, 2026
795 views
3
4.2 (15)
Implement a small controlled form that validates a single text input is non-empty. On submit, show an error list above the button if validation fails. Use hooks, no libraries.
What I want to see
Two pieces of state (inputValue and errors), an onChange that updates the value, an onSubmit that runs validation synchronously and sets errors. The bug in the starter is that errors is referenced in JSX but never declared.
A teammate's PR converted an uncontrolled form (ref + defaultValue) to a fully controlled form (state + value). The page is now janky on large forms (30+ fields). Walk through when controlled is the right choice, when uncontrolled is, and how you would refactor without going all-in on either.
What I want to see
Named trade-offs: controlled gives instant validation and field interdependencies but re-renders every keystroke; uncontrolled reads on submit and is faster but harder to validate inline. The pragmatic answer: hybrid, with most fields uncontrolled (refs) and only the few that drive UI updates controlled.
When does react-hook-form beat a hand-rolled hooks form, and when is it overkill? My teammate wanted to add it to a 2-field login page. I pushed back. What did I tell them?
What I want to see
The call I usually make: react-hook-form pays off at 5+ fields, when you need field-level validation rules, async resolvers, or want to avoid the Context-of-form-state re-render storm. For a 2-field login the bundle cost is bigger than the typing saved.
I have a username field that checks availability against the server on every change. Users with a fast connection see flicker; users with a slow one see the wrong availability after they keep typing. Walk through the race condition and write a debounced async validator that cannot show a stale result.
What I want to see
The stale-result problem: request A for 'ali' is in flight, user types 'alic', request B fires, but A returns last and overwrites. The fix is either an AbortController or a request-id token that the response compares against the latest issued id.
On the last accessibility audit my form failed because errors were not associated with the inputs they described. Screen readers announced the input as Email and never reached the error. Wire up the field so the error is announced as part of the input's accessible description.
What I want to see
aria-invalid on the input when there is an error, an id on the error element, and aria-describedby on the input pointing to that id. Bonus: an aria-live region for the error container so the new text is announced when it appears.
