PatternFormValidationInput

Form Validation Pattern

A composable form validation pattern with custom validators, real-time feedback, and TypeScript support.

Preview

Code

import { useState, useCallback } from 'react'

type Validator<T> = (value: T) => string | null

interface FieldState<T> {
  value: T
  error: string | null
  touched: boolean
}

interface FormField<T> {
  state: FieldState<T>
  onChange: (value: T) => void
  onBlur: () => void
  validate: () => boolean
  reset: () => void
}

export function useFormField<T>(
  initialValue: T,
  validators: Validator<T>[] = []
): FormField<T> {
  const [state, setState] = useState<FieldState<T>>({
    value: initialValue,
    error: null,
    touched: false,
  })

  const runValidation = useCallback(
    (value: T): string | null => {
      for (const validator of validators) {
        const error = validator(value)
        if (error) return error
      }
      return null
    },
    [validators]
  )

  const onChange = useCallback(
    (value: T) => {
      const error = state.touched ? runValidation(value) : null
      setState({ value, error, touched: state.touched })
    },
    [runValidation, state.touched]
  )

  const onBlur = useCallback(() => {
    setState((prev) => ({
      ...prev,
      touched: true,
      error: runValidation(prev.value),
    }))
  }, [runValidation])

  const validate = useCallback(() => {
    const error = runValidation(state.value)
    setState((prev) => ({ ...prev, touched: true, error }))
    return error === null
  }, [runValidation, state.value])

  const reset = useCallback(() => {
    setState({ value: initialValue, error: null, touched: false })
  }, [initialValue])

  return { state, onChange, onBlur, validate, reset }
}

// Validators
export const required = (msg = 'Required'): Validator<string> =>
  (value) => (value.trim() ? null : msg)

export const minLength = (min: number, msg?: string): Validator<string> =>
  (value) => (value.length >= min ? null : msg || `Min ${min} characters`)

export const email = (msg = 'Invalid email'): Validator<string> =>
  (value) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : msg)

// Usage example:
function SignupForm() {
  const emailField = useFormField('', [required(), email()])
  const passwordField = useFormField('', [required(), minLength(8)])

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (emailField.validate() && passwordField.validate()) {
      console.log('Submit:', emailField.state.value, passwordField.state.value)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={emailField.state.value}
        onChange={(e) => emailField.onChange(e.target.value)}
        onBlur={emailField.onBlur}
      />
      {emailField.state.error && <span>{emailField.state.error}</span>}
      {/* ... */}
    </form>
  )
}

Looking for more?

Browse the full collection of patterns or check out other exploration topics.