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

  1. Why compose small validators instead of one large function?
  2. When would you add async validation to a form?
Show answers
  1. Composability improves reuse and clarity; small rules are easier to test and maintain.
  2. When validating server-backed constraints (e.g., username availability) or remote policies.

Exercises

  1. Add a confirm password field with cross-field validation.
  2. Display errors on blur and on submit; include a summary list above the form.
  3. Swap the custom schema with a Zod schema and map errors into the same format.