In a React form, why is running validation on every keystroke a poor default, and what extra state do you keep so a field's error appears at the right moment?
answer
- errors before they finished typing
- validity and display are separate
- blur marks a field touched
- validate late, clear early
- submit is the backstop, server is the authority
basics
~20 sValidating on every keystroke shows an error before the user has finished typing, so a half-typed email looks broken. Track a per-field touched flag set on blur, plus a submitted flag, and render an error only once one of them is true.
solid answer
~50 sValidation and *showing* the result are two different moments, and the usual mistake is fusing them. If I validate and render on every change, the user sees "invalid email" after the first character — the form is scolding them for not having finished. So I compute validity from the current values whenever I need it, but gate the display on interaction state: a `touched` map, with the field's key set on `onBlur`, and a `submitted` flag flipped in the submit handler. An error renders when `touched[name] || submitted`. The refinement everyone expects you to add is that once a field has shown an error, you switch that field to validating on change, so the message disappears the moment the input becomes valid instead of waiting for another blur. Validate late, clear early. And on submit I validate everything, not just touched fields, and move focus to the first invalid one.
code
javascript · 39 linesimport { useState } from 'react';
function validate({ email, password }) {
const errors = {};
if (!email.includes('@')) errors.email = 'Enter a valid email address';
if (password.length < 8) errors.password = 'Use at least 8 characters';
return errors;
}
export default function SignupForm() {
const [values, setValues] = useState({ email: '', password: '' });
const [touched, setTouched] = useState({});
const [submitted, setSubmitted] = useState(false);
const errors = validate(values);
const show = name => Boolean(errors[name]) && (touched[name] || submitted);
const handleChange = e =>
setValues(prev => ({ ...prev, [e.target.name]: e.target.value }));
const handleBlur = e =>
setTouched(prev => ({ ...prev, [e.target.name]: true }));
function handleSubmit(e) {
e.preventDefault();
setSubmitted(true);
if (Object.keys(errors).length > 0) return;
console.log('send', values);
}
return (
<form onSubmit={handleSubmit} noValidate>
<input name="email" value={values.email} onChange={handleChange} onBlur={handleBlur} />
{show('email') && <p>{errors.email}</p>}
<input name="password" type="password" value={values.password} onChange={handleChange} onBlur={handleBlur} />
{show('password') && <p>{errors.password}</p>}
<button type="submit">Sign up</button>
</form>
);
}go deeper
Know that blur is the usual moment to first show a field error and that the submit handler must check every field, not just the ones the user visited.
Explain the state you add — a touched map and a submitted flag — and why validity is derived from values while display is gated on interaction. Name the blur-then-change asymmetry.
Show the production details: focus management to the first invalid field, debounced async checks that ignore stale responses, and the plain statement that client validation is UX while the server is the authority.
Own where the rules live. Argue for one shared schema over per-form hand-written checks, decide how server-returned field errors are merged into the same display path, and set a house standard so every form in the product misbehaves in the same predictable way.
## Two separate questions Every form has to answer two things that candidates routinely merge: *is this value valid?* and *should the user be looking at an error right now?* Validity is a pure function of the current values. Display is a function of what the user has done — whether they have finished with this field, whether they have tried to submit. Keeping them separate is the whole trick. ## Why change-time display is hostile Type `a` into an email field. If the error renders on change, the user immediately sees "Enter a valid email address" — for a field they are one character into. The message is technically true and practically useless: it fires while the value is still under construction, so it reads as the form calling the user wrong before they have had a chance to be right. Password fields are worse: "too short" flashes on every character until it suddenly does not. There is a secondary cost. Change-time validation runs on every keystroke, so anything expensive — a regex over a long string, a schema parse of the whole form, an async uniqueness check — runs at typing frequency. ## The state that fixes it Three pieces of state, beyond the values themselves: ```jsx const [values, setValues] = useState({ email: '', password: '' }); const [touched, setTouched] = useState({}); const [submitted, setSubmitted] = useState(false); const errors = validate(values); // pure: values in, { field: message } out const showError = name => Boolean(errors[name]) && (touched[name] || submitted); ``` `onBlur={e => setTouched(prev => ({ ...prev, [e.target.name]: true }))}` marks a field as finished with. The submit handler sets `submitted` so a user who tabs straight to the button still sees every problem. Note that `errors` here is derived on each render rather than stored — one less thing that can go stale against the values it describes. ## Validate late, clear early Blur-only display has one annoying property: after the user goes back to fix a field, the error keeps sitting there while they type the correction, and only clears when they leave again. So the accepted pattern is asymmetric — **validate on blur to decide when to first show an error, then validate that field on change so the error clears the instant it is fixed.** With the derived-`errors` shape above you get this for free: `errors` recomputes on every render, so once `touched[name]` is true the message disappears as soon as the value becomes valid. This asymmetry is exactly what mainstream form libraries default to, and naming it is a strong signal in an interview. ## Submit is the backstop On submit: validate everything, do not trust the touched map, and if anything fails, prevent the submission, set `submitted`, and move focus to the first invalid field so a keyboard user is not stranded at the bottom of the form. Rendering ten error messages nobody's focus is near is not a fixed form. ## Async validation is a different beast "Is this username taken?" cannot run on blur alone if you want it responsive, and cannot run per keystroke without hammering the endpoint. The shape is: debounce the trigger, keep a per-field pending state so the UI can say "checking…", and make sure a late response for an older value cannot overwrite the verdict for the current one. Treat the server's answer as advisory for the UI only. ## Client validation is UX, not enforcement Whichever moment you pick, the client-side rules exist to give fast feedback. Anything that actually matters — uniqueness, authorization, business rules, the final shape of the data — is re-validated on the server, because the client is fully under the user's control. A candidate who describes an elaborate client validation scheme and never mentions the server has answered half the question. ## What good looks like A compact summary you can say out loud: errors are derived from values, display is gated on `touched || submitted`, first display happens on blur, clearing happens on change, submit validates everything and focuses the first failure, and the server validates again regardless.
- Why compute the errors object during render instead of storing it in state and updating it in the change handler?Stored errors can disagree with the values they describe — one handler forgets to recompute, or an update lands from somewhere else, and the message is now about a value that no longer exists. Deriving errors from the current values on each render makes that class of bug impossible and removes an update path. The only thing worth storing is interaction state: touched and submitted.
- How do you handle a validation rule that needs the server, such as whether a username is already taken?Debounce the trigger so it does not fire per keystroke, keep a pending flag per field so the UI can show a checking state, and guard against a stale response overwriting the verdict for a newer value — usually by discarding a result whose input no longer matches the current one. Treat the answer as UI feedback only; the server re-checks uniqueness at submit time anyway, since two users can pass the check simultaneously.
- A user tabs straight to the submit button without touching any field. What does your form do?The submit handler prevents the default submission, sets the submitted flag, and validates the whole values object rather than only touched fields — so every error becomes visible at once. Then it moves focus to the first invalid field, because a keyboard user who is on the button at the bottom of the form otherwise has no idea where the problem is.
saying these in an interview costs you the question
- Validates and displays on change, so errors flash mid-typing
- Keeps no touched state, so a pristine form renders red
- Only ever validates in the submit handler
- Stores errors in state and lets them drift from the values
- Treats client-side validation as the security boundary
- Fires an async uniqueness check on every keystroke