DEV Community

Thesius Code
Thesius Code

Posted on • Originally published at datanest-stores.pages.dev

Form Validation Library

Form Validation Library

Battle-tested form patterns built on React Hook Form and Zod that handle the hard parts of form development: multi-step wizards with state persistence, async field validation, file upload with drag-and-drop, dependent field logic, accessible error announcements, and type-safe form schemas that double as API request validators. Includes 20+ ready-to-use form templates covering authentication, checkout, profile editing, surveys, and admin CRUD — each with full TypeScript types and zero runtime dependencies beyond React Hook Form and Zod.

Key Features

  • Zod Schema Validation — Type-safe schemas that validate on the client and reuse on the server; define once, validate everywhere
  • Multi-Step Form Wizard — Step navigation with validation-per-step, progress persistence to sessionStorage, and back/forward support
  • Accessible Error Handling — Errors announced to screen readers via aria-live, focus management on submission failure, and inline validation
  • File Upload Patterns — Drag-and-drop zone with preview, file type/size validation, multi-file uploads, and upload progress tracking
  • Dependent Fields — Watch-based conditional rendering where field B's options change based on field A's value, with proper cleanup
  • Async Validation — Debounced server-side validation (e.g., "is this username taken?") with loading indicators
  • Form State Persistence — Auto-save drafts to localStorage with configurable debounce, restore on page reload
  • Server Action Integration — Patterns for Next.js Server Actions with progressive enhancement and optimistic UI

Quick Start

  1. Install dependencies:
npm install react-hook-form zod @hookform/resolvers
Enter fullscreen mode Exit fullscreen mode
  1. Copy the forms/ directory into your project's src/lib/ folder.

  2. Create a validated form in under 30 lines:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { FormField, FormError } from '@/lib/forms/components';

const loginSchema = z.object({
  email: z.string().email('Enter a valid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

type LoginForm = z.infer<typeof loginSchema>;

export function LoginForm({ onSubmit }: { onSubmit: (data: LoginForm) => void }) {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<LoginForm>({
    resolver: zodResolver(loginSchema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <FormField label="Email" error={errors.email}>
        <input type="email" {...register('email')} aria-invalid={!!errors.email} />
      </FormField>
      <FormField label="Password" error={errors.password}>
        <input type="password" {...register('password')} aria-invalid={!!errors.password} />
      </FormField>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Signing in...' : 'Sign In'}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Architecture / How It Works

form-validation-library/
├── schemas/              # Zod validation schemas (auth, profile, checkout, common)
├── components/           # FormField, FormError, FileUpload, MultiStepForm, AsyncSelect
├── hooks/                # useFormPersist, useAsyncValidation, useDependentFields
├── templates/            # LoginForm, RegistrationWizard, CheckoutForm, ContactForm
└── utils/                # Formatting and parsing utilities
Enter fullscreen mode Exit fullscreen mode

Usage Examples

Multi-Step Form Wizard

import { MultiStepForm, Step } from '@/lib/forms/components';
import { personalInfoSchema, addressSchema, paymentSchema } from '@/lib/forms/schemas';

export function CheckoutWizard() {
  return (
    <MultiStepForm
      onComplete={async (data) => {
        await fetch('/api/orders', { method: 'POST', body: JSON.stringify(data) });
      }}
      persistKey="checkout-draft"
    >
      <Step schema={personalInfoSchema} title="Personal Info">
        {({ register, errors }) => (
          <>
            <FormField label="Full Name" error={errors.name}>
              <input {...register('name')} />
            </FormField>
            <FormField label="Email" error={errors.email}>
              <input type="email" {...register('email')} />
            </FormField>
          </>
        )}
      </Step>
      <Step schema={addressSchema} title="Shipping Address">
        {({ register, errors }) => (/* address fields */)}
      </Step>
      <Step schema={paymentSchema} title="Payment">
        {({ register, errors }) => (/* payment fields */)}
      </Step>
    </MultiStepForm>
  );
}
Enter fullscreen mode Exit fullscreen mode

Configuration

Zod Custom Error Messages

// schemas/common.ts — reusable field validators
export const emailField = z
  .string()
  .min(1, 'Email is required')
  .email('Enter a valid email address')
  .transform((v) => v.toLowerCase().trim());

export const passwordField = z
  .string()
  .min(8, 'Must be at least 8 characters')
  .regex(/[A-Z]/, 'Must contain an uppercase letter')
  .regex(/[0-9]/, 'Must contain a number');
Enter fullscreen mode Exit fullscreen mode

Form Persistence Config

useFormPersist({
  key: 'checkout-form',
  debounceMs: 500,         // save after 500ms of inactivity
  storage: sessionStorage,  // clear on tab close
  exclude: ['password', 'cvv'],  // never persist sensitive fields
});
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Validate on blur, not on changemode: 'onBlur' prevents error messages from appearing while the user is still typing
  • Use Zod's .transform() — Normalize data (trim strings, lowercase emails) at the schema level, not in submit handlers
  • Show errors inline, not in a banner — Users fix errors 40% faster when the message is next to the field
  • Persist multi-step form state — Use useFormPersist so users don't lose progress on page refresh or accidental navigation
  • Mark required fields, not optional ones — Most fields are required; mark the exceptions with "(optional)"
  • Disable submit during async validation — Prevent double-submissions and race conditions with isSubmitting state

Troubleshooting

Issue Cause Fix
Zod errors don't show on fields zodResolver not connected to useForm Pass resolver: zodResolver(schema) in useForm options
File upload doesn't clear after submit File input value not reset Call reset() from useForm and clear the file input ref
Multi-step form loses state on back navigation Each step unmounts and remounts Use useFormPersist with sessionStorage to retain values
TypeScript errors on nested form fields register path doesn't match schema shape Use z.object().merge() or .extend() for nested structures

This is 1 of 11 resources in the Frontend Developer Pro toolkit. Get the complete [Form Validation Library] with all files, templates, and documentation for $19.

Get the Full Kit →

Or grab the entire Frontend Developer Pro bundle (11 products) for $129 — save 30%.

Get the Complete Bundle →


Related Articles

Top comments (0)