javascript 129 lines · 4 tabs

Field-by-Field Signup Validation with a Reusable useForm Hook in React

Shared by codesnips Aug 2026
4 tabs
export const required = (msg = 'This field is required') => (value) =>
  value && value.trim() !== '' ? '' : msg;

export const minLength = (n, msg) => (value) =>
  value && value.length >= n ? '' : msg || `Must be at least ${n} characters`;

export const isEmail = (msg = 'Enter a valid email address') => (value) =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value || '') ? '' : msg;

export const matches = (getOther, msg = 'Values do not match') => (value, values) =>
  value === getOther(values) ? '' : msg;

export const compose = (...rules) => (value, values) => {
  for (const rule of rules) {
    const error = rule(value, values);
    if (error) return error;
  }
  return '';
};
4 files · javascript Explain with highlit

This snippet demonstrates a pragmatic approach to form validation in React: a single reusable useForm hook that owns field values, tracks which fields have been touched, and runs a declarative validation schema per field. The goal is to validate as the user progresses — showing an error only after a field is blurred — rather than blasting every error on first render or waiting until submit.

In useForm hook, state is split into three maps: values, touched, and derived errors. The validators argument is a plain object mapping each field name to a function returning an error string or an empty value. runValidators is memoized with useCallback so it can safely feed a useMemo that recomputes errors only when values or validators change; this keeps validation cheap and pure. handleChange updates the value, while handleBlur flips the touched flag so the UI can reveal an error at the right moment. handleSubmit marks every field touched and blocks the callback when any validator still fails, covering the case where a user hits submit without ever touching a field.

validators.js holds the actual rules as small composable predicates — required, minLength, isEmail, and a compose helper that returns the first failing message. Keeping rules outside the component makes them unit-testable and shareable across forms, and compose short-circuits so the most relevant message wins.

FieldError component is a tiny presentational component that renders nothing unless the field is both touched and invalid. It wires role="alert" and an id so the input can reference it via aria-describedby, which matters for screen-reader users who otherwise never hear inline errors.

SignupForm component ties it together: it spreads getFieldProps onto each input to bind value, onChange, and onBlur in one call, sets aria-invalid, and disables submit while the form is invalid. The trade-off of this hook-based approach is that it re-renders on every keystroke, which is fine for small forms but would warrant a library like React Hook Form for very large or deeply nested ones. For a signup screen, though, this keeps the logic transparent, dependency-free, and easy to extend with new fields or rules.


Related snips

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 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
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
swift
import SwiftUI

struct ContentView: View {
    @State private var username = ""
    @State private var isLoggedIn = false
    @StateObject private var viewModel = LoginViewModel()

SwiftUI declarative UI with state management

swift swiftui ios
by Sofia Martinez 2 tabs

Share this code

Here's the card — post it anywhere.

Field-by-Field Signup Validation with a Reusable useForm Hook in React — share card
Link copied