typescript 134 lines · 3 tabs

Field-Level Form Validation with a Reusable useField Hook in React

Shared by codesnips Aug 2026
3 tabs
export type Validator = (value: string) => string | null;

export const required = (message = 'This field is required'): Validator => {
  return (value) => (value.trim().length === 0 ? message : null);
};

export const minLength = (n: number, message?: string): Validator => {
  return (value) =>
    value.length < n ? message ?? `Must be at least ${n} characters` : null;
};

export const email = (message = 'Enter a valid email'): Validator => {
  const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return (value) => (re.test(value) ? null : message);
};

export function runValidators(value: string, validators: Validator[]): string | null {
  for (const validate of validators) {
    const result = validate(value);
    if (result !== null) return result;
  }
  return null;
}
3 files · typescript Explain with highlit

This snippet shows a lightweight, dependency-free approach to form validation in React where each field owns its own state, validation rules, and error message rather than relying on a single monolithic form object. The core idea is that validation happens per field, on blur and on change, so users get immediate, scoped feedback instead of a wall of errors on submit.

In validators.ts, a Validator is just a function that takes a value and returns either null (valid) or a string error message. Small combinators like required, minLength, and email are factory functions that close over their parameters and return a Validator. The runValidators helper runs an ordered list and returns the first failing message, which mirrors how users expect to fix one problem at a time. Keeping validators as plain functions makes them trivially composable and testable in isolation.

The useField hook encapsulates everything a single input needs: its value, a touched flag, and a derived error. Validation runs eagerly inside handleChange only after the field has been touched, so a pristine field never shows red. handleBlur marks the field touched and forces a validation pass, catching the case where a user tabs through an empty required field. The hook exposes a validate method so a parent form can trigger every field imperatively on submit, and returns a ready-to-spread inputProps object to keep JSX clean.

In SignupForm, three independent useField calls each carry their own validator chain. The errorMap is built by reading each field's error, giving a single object suitable for rendering a summary or gating the submit button. On submit, handleSubmit calls every field's validate() and only proceeds when all pass — the Boolean reduce avoids short-circuiting so all errors surface at once.

The trade-off is a little boilerplate per field versus a schema library, but the payoff is full control, zero dependencies, and transparent state. This pattern scales well for small-to-medium forms and is easy to extend with async validators or debouncing later.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 2 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs

Share this code

Here's the card — post it anywhere.

Field-Level Form Validation with a Reusable useField Hook in React — share card
Link copied