Community JavaScript Snippet
useFormField Hook With Field-Level Validation
A field-scoped form hook for the cases where react-hook-form is overkill. Tracks value, touched, blur, and an async validator with a single race-safe in-flight token.
useFormField Hook With Field-Level Validation
A field-scoped form hook for the cases where react-hook-form is overkill. Tracks value, touched, blur, and an async validator with a single race-safe in-flight token.
By @leoeriksson
February 23, 2026
·
Updated May 20, 2026
729 views
13
4.5 (10)
The trick to making useFormField ergonomic is the inputProps object: consumers spread it on a JSX element and the hook owns every piece of behavior. I keep error as a derived value from useMemo, not state, because then validators are pure and a fresh validator function (closing over a different prop) takes effect immediately. The showError boolean encapsulates the rule that we only show the error AFTER the user has blurred the field once, which is the single biggest reason form UX feels respectful or hostile. Reset takes the form back to its original state and is the function I forget to expose half the time.
Async validators are where most hand-rolled form hooks ship a bug. If the user types taken, then immediately corrects to alex, two validate() calls are in flight; if the taken response lands second, the field shows an error for a value that is no longer present. The tokenRef increments on every effect run; when a response comes back, we drop it unless it is still the latest token. The validating flag is what powers the spinner next to the field, and showError: touched && !!error && !validating is the rule that prevents the error message from blinking on every keystroke.
This is the shape of every login or contact form I write that does not deserve react-hook-form (the threshold for me is roughly four fields). Each field is one hook call; canSubmit is a single AND across the field errors; the submit handler nudges every field to touched so error messages appear when the user clicks the disabled button. The submit result returns an object instead of throwing because the most common consumer is a parent that wants to show a toast on failure, not a global error boundary. For anything bigger, I graduate to a real form library, but the upgrade path is trivial because the validators are already pure.
