Rules and the validation progression
Forms are where data, validation and UX meet. We'll build a settings form (name, email, theme, defaults) that actually changes the app and persists. And most of all, we'll learn when to show errors.
Reusable rules. Instead of rewriting validation for every form, we make pure functions that compose:
const rules = {
displayName: [required('Display name'), maxLength(40, 'Display name')],
email: [email()], // optional, but if present it must be valid
avatarUrl: [url()],
};
Each rule is a function returning an error string or null. They have no DOM and no state, so they're easy to test.
The validation progression — the most important part of this lesson. When do you check a field? There are three stages, and they're taught in this order:
- On submit. The simplest — validate everything on submit. But the user finds out about an error late, after filling the whole form.
- On blur. When the user leaves a field, validate it immediately. Faster feedback.
- On keystroke — but only once an error is showing. Once a field already shows an error, validate on every keystroke, so the error clears the moment they fix it.
The third stage is subtle. Validating on every keystroke from the start is hostile: "Email is invalid" after the first letter is annoying. But once you've already complained, continuing to check becomes helpful — the user sees the error disappear as they fix it. That's real UX judgment, written in code.
And a real CSS trap awaits, one we hit ourselves: half the inputs looked different from the others.