typescriptintermediate

Controlled Form Component

A reusable controlled form pattern with field-level validation and submission handling.

typescript
import { useState, FormEvent } from 'react';

type Errors<T> = Partial<Record<keyof T, string>>;
type Validator<T> = (values: T) => Errors<T>;

export function useForm<T extends Record<string, unknown>>(initial: T, validate: Validator<T>) {
  const [values, setValues] = useState(initial);
  const [errors, setErrors] = useState<Errors<T>>({});
  const [touched, setTouched] = useState<Set<keyof T>>(new Set());

  const handleChange = (field: keyof T, value: T[keyof T]) => {
    setValues(prev => ({ ...prev, [field]: value }));
    if (touched.has(field)) {
      const newErrors = validate({ ...values, [field]: value });
      setErrors(prev => ({ ...prev, [field]: newErrors[field] }));
    }
  };

  const handleBlur = (field: keyof T) => {
    setTouched(prev => new Set(prev).add(field));
    const newErrors = validate(values);
    setErrors(prev => ({ ...prev, [field]: newErrors[field] }));
  };

  const handleSubmit = (onSubmit: (values: T) => void) => (e: FormEvent) => {
    e.preventDefault();
    const newErrors = validate(values);
    setErrors(newErrors);
    if (Object.keys(newErrors).length === 0) onSubmit(values);
  };

  return { values, errors, handleChange, handleBlur, handleSubmit };
}

Use Cases

  • Login forms
  • Settings pages

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.