typescript 108 lines · 3 tabs

React Hook Form + Zod resolver

Shared by codesnips Jan 2026
3 tabs
import { z } from 'zod';

export const signupSchema = z
  .object({
    email: z.string().min(1, 'Email is required').email('Enter a valid email'),
    password: z
      .string()
      .min(8, 'Password must be at least 8 characters')
      .regex(/[0-9]/, 'Include at least one number'),
    confirmPassword: z.string().min(1, 'Please confirm your password'),
    acceptTerms: z.literal(true, {
      errorMap: () => ({ message: 'You must accept the terms' }),
    }),
  })
  .refine((data) => data.password === data.confirmPassword, {
    path: ['confirmPassword'],
    message: 'Passwords do not match',
  });

export type SignupValues = z.infer<typeof signupSchema>;
3 files · typescript Explain with highlit

This snippet shows how a form's runtime validation and its compile-time types can be derived from a single source of truth using Zod, then wired into React Hook Form through the @hookform/resolvers/zod resolver. The pattern removes the classic drift between hand-written TypeScript interfaces and hand-written validation logic: the schema declares both.

In signup.schema.ts, signupSchema describes the shape and constraints of the form — email format, password length, and a cross-field refine that ensures password and confirmPassword match. The refine attaches its error to a specific path, so the message surfaces on the confirmPassword field rather than at the form root. z.infer<typeof signupSchema> then produces the SignupValues type, which every consumer imports; changing the schema automatically updates the types downstream.

The useSignupForm hook centralizes the RHF setup. Passing zodResolver(signupSchema) to useForm means every submit and every configured validation trigger runs the input through Zod, and validation errors are mapped back onto formState.errors keyed by field name. mode: 'onTouched' is chosen so fields validate after first blur rather than on every keystroke, which avoids flashing errors while the user is still typing. The hook wraps handleSubmit with the caller-supplied onValid, keeping the async submission concern out of the component.

SignupForm.tsx consumes the hook and stays purely presentational. Because register is typed against SignupValues, a typo in a field name is a compile error rather than a silent no-op. Each input reads its error from errors, and the submit button disables while isSubmitting is true to prevent double submits.

The key trade-off is that all validation lives in the schema, so complex conditional rules require Zod features like superRefine or discriminated unions rather than ad-hoc component code. In exchange, the form gets one authoritative definition, end-to-end type safety, and validation that is trivially reusable on a server that shares the same schema.


Related snips

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
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 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
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab

Share this code

Here's the card — post it anywhere.

React Hook Form + Zod resolver — share card
Link copied