React - Forms: Schema Validation
Overview
Estimated time: 25–35 minutes
Create a small, schema-driven validator (fully runnable) and see how a Zod-style schema would look (shown as a snippet). This mirrors production form validation patterns.
Try it: Custom schema validation (runnable)
View source
const { useState } = React;
function required(msg='Required'){ return v => (v ? '' : msg); }
function minLen(n, msg=`Min length ${n}`){ return v => (v && v.length >= n ? '' : msg); }
function email(msg='Invalid email'){ return v => (/.+@.+\..+/.test(v||'') ? '' : msg); }
function compose(...rules){ return v => rules.map(r => r(v)).find(x => x) || ''; }
function validate(schema, values){
const errors = {};
for (const k in schema){
const e = schema[k](values[k]);
if (e) errors[k] = e;
}
return errors;
}
function Form(){
const schema = {
name: compose(required(), minLen(3)),
email: compose(required(), email()),
password: compose(required(), minLen(6)),
};
const [values, setValues] = useState({ name:'', email:'', password:'' });
const [errors, setErrors] = useState({});
function update(k){ return e => { const v = e.target.value; setValues(s => ({...s,[k]:v})); setErrors(s => ({...s, [k]: schema[k](v)})); } }
function submit(e){
e.preventDefault();
const es = validate(schema, values);
setErrors(es);
if (Object.keys(es).length===0){ alert('OK: '+JSON.stringify(values)); }
}
return (
<form onSubmit={submit}>
<label>Name: <input value={values.name} onChange={update('name')} /></label> {errors.name && <span style={{color:'salmon'}}>{errors.name}</span>}
<br />
<label>Email: <input value={values.email} onChange={update('email')} /></label> {errors.email && <span style={{color:'salmon'}}>{errors.email}</span>}
<br />
<label>Password: <input type="password" value={values.password} onChange={update('password')} /></label> {errors.password && <span style={{color:'salmon'}}>{errors.password}</span>}
<div style={{marginTop:8}}><button>Submit</button></div>
</form>
);
}
ReactDOM.createRoot(document.getElementById('try-schema')).render(<Form />);
Optional: Zod-style schema (snippet)
View Zod snippet (not executed)
// Example only (not executed in this page)
// const schema = z.object({
// name: z.string().min(3),
// email: z.string().email(),
// password: z.string().min(6),
// });
// const res = schema.safeParse(values);
// if (!res.success) { /* map res.error.issues to field messages */ }
Syntax primer
- Compose small rules to build per-field validators.
- Run validation onChange/onBlur for live feedback and again on submit.
Common pitfalls
- Inconsistent error formats – standardize to a simple map of field to message.
- Missing async validation (e.g., checking username availability) – add as needed.
Checks for Understanding
- Why compose small validators instead of one large function?
- When would you add async validation to a form?
Show answers
- Composability improves reuse and clarity; small rules are easier to test and maintain.
- When validating server-backed constraints (e.g., username availability) or remote policies.
Exercises
- Add a confirm password field with cross-field validation.
- Display errors on blur and on submit; include a summary list above the form.
- Swap the custom schema with a Zod schema and map errors into the same format.