Building a Login Form in React: One Handler, Two States, and Zero Stale Closures
Multi-field forms are where React state management stops being obvious: a shared handler, separate validation state, and derived booleans prevent the sprawl of per-field onChange functions and out-of-sync isValid flags that plague quick-and-dirty form code. The pattern scales to any number of fields without adding complexity.
Starting from a single controlled input, the form grows field by field: first a shared onChange handler that routes updates by `e.target.name`, then a separate `errors` state that holds validation results as strings. The validation function receives the new value as a parameter to avoid reading a stale closure, and it writes error messages with a functional `setErrors(prev => ...)` so rapid keystrokes never overwrite each other. An `isValid` boolean is computed on every render from `form` and `errors`—no dedicated useState needed—and drives both the submit button's `disabled` prop and a guard clause inside `onSubmit`.
The design hinges on a few deliberate choices. An empty string `""` doubles as "no error" and "clear the error," which makes conditional rendering with `&&` safe and eliminates explicit error-clearing logic. The `validate` function never reads `form` directly because `setForm` only queues an update; passing the value as an argument keeps the check in sync with the latest keystroke. React's automatic batching merges the data and error state updates into a single render, so the input value and any error message always change in the same frame.
The tutorial's progression mirrors how form complexity actually grows in a codebase: start with one field, hit pain, refactor—rather than presenting a finished pattern with no context for why each piece exists.
Using an empty string as both 'no error' and 'clear error' is a subtle design move that eliminates an entire class of state-management bugs; many developers reach for null or undefined and then need extra logic to distinguish 'unchecked' from 'valid.'
The distinction between passing an object vs. a function to setState is often treated as a React trivia question, but the validate-and-overwrite race condition shown here is a concrete, reproducible bug that the functional form prevents.
The barrel export pattern shown at the end is a small organizational detail that pays off as component count grows, yet it's frequently omitted from form tutorials that focus only on hooks.