React Hook Form

Master React Hook Form — the industry-standard library for forms in React. Learn registration, validation with Zod, error handling, and the patterns used in enterprise applications.

Last reviewed: January 2025

·React 16+React 18React Hook Form 7.x
✅
Quick Understanding: React Hook Form (RHF) is the best way to handle forms in React. It uses uncontrolled inputs under the hood (no re-render on every keystroke), integrates with schema validation libraries like Zod, and provides a simple API for registration, validation, and submission. It handles 95% of form use cases with minimal code.

🔍 What Is It?

React Hook Form provides a simple hook-based API for form management:

const { register, handleSubmit, formState: { errors } } = useForm<FormData>();

Core API:

  • →register(name, options) — connects an input to RHF (returns ref, onChange, onBlur, name)
  • →handleSubmit(fn) — validates the form, then calls your submit function with the data
  • →formState.errors — validation errors for each field
  • →watch(name) — subscribe to a field's current value
  • →setValue(name, value) — programmatically set a field value
  • →reset(values) — reset the form (optionally with new default values)
  • →Controller — wrapper for controlled third-party inputs (Select, DatePicker, etc.)

🎯 Why Do We Need It?

Manual controlled forms re-render on every keystroke. For a form with 20 fields, typing one character re-renders the entire form. RHF solves this:

  • →Performance — uncontrolled inputs, renders only on submit/error
  • →Less code — no manual state management for each field
  • →Built-in validation — required, min, max, pattern, custom validators
  • →Schema validation — Zod/Yup integration for type-safe validation
  • →TypeScript first — fully typed form data and errors
  • →DevTools — React Hook Form DevTools for debugging

🏗️ What Happens in Real Projects?

RHF is the standard for forms in enterprise React:

  • →Multi-step insurance claim submission forms
  • →Patient registration forms in healthcare
  • →Financial product application forms (mortgage, loan)
  • →User profile and settings forms
  • →Dynamic forms where fields appear based on other field values

⚙️ How It Works

RHF uses uncontrolled inputs — it attaches a ref to each input and reads values directly from the DOM when needed (on submit or validation trigger). This avoids the re-render-on-every-keystroke problem of controlled inputs.

Validation triggers:

  • →mode: 'onSubmit' (default) — validate only on submit
  • →mode: 'onChange' — validate on every change
  • →mode: 'onBlur' — validate when field loses focus
  • →mode: 'onTouched' — validate on blur, then on change after first blur

Schema validation:

Integrate with Zod via @hookform/resolvers/zod. Define your schema once, get TypeScript types and runtime validation for free.

💻 Example

React Hook Form with Zod Validation — Complete form with schema validation, error handling, and API submission.

tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Define schema — single source of truth for validation and types
const claimSchema = z.object({
  policyNumber: z.string().regex(/^POL-d{6}$/, 'Format: POL-123456'),
  claimType: z.enum(['medical', 'property', 'vehicle'], {
    errorMap: () => ({ message: 'Select a claim type' }),
  }),
  incidentDate: z.string().min(1, 'Incident date is required'),
  description: z.string().min(20, 'Describe the incident (min 20 characters)'),
  amount: z.number({ invalid_type_error: 'Enter a valid amount' }).positive('Amount must be positive'),
});

type ClaimFormData = z.infer<typeof claimSchema>; // TypeScript type from schema

export function NewClaimForm({ onSuccess }: { onSuccess: () => void }) {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError,
    reset,
  } = useForm<ClaimFormData>({
    resolver: zodResolver(claimSchema),
    defaultValues: {
      policyNumber: '',
      claimType: undefined,
      incidentDate: '',
      description: '',
      amount: undefined,
    },
  });

  const onSubmit = async (data: ClaimFormData) => {
    try {
      await submitClaim(data);
      reset();
      onSuccess();
    } catch (err) {
      // Set server-side errors back to specific fields
      setError('policyNumber', { message: 'Policy not found or inactive' });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div className="field">
        <label htmlFor="policyNumber">Policy Number</label>
        <input
          id="policyNumber"
          {...register('policyNumber')}
          placeholder="POL-123456"
          aria-invalid={!!errors.policyNumber}
        />
        {errors.policyNumber && (
          <p className="error" role="alert">{errors.policyNumber.message}</p>
        )}
      </div>

      <div className="field">
        <label htmlFor="claimType">Claim Type</label>
        <select id="claimType" {...register('claimType')}>
          <option value="">Select type</option>
          <option value="medical">Medical</option>
          <option value="property">Property Damage</option>
          <option value="vehicle">Vehicle</option>
        </select>
        {errors.claimType && <p className="error">{errors.claimType.message}</p>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit Claim'}
      </button>
    </form>
  );
}

🤔 Common Confusions

❓ When should I use register vs Controller?

Use register for native HTML inputs (input, select, textarea) — it attaches a ref directly. Use Controller for third-party components that do not expose a ref (custom Select, DatePicker, Slider, rich text editor). Controller provides a render prop with onChange, onBlur, and value that you pass to the third-party component.

❓ How do I watch a field value to show/hide other fields?

Use the watch function: const claimType = watch("claimType"). This subscribes to the field and returns its current value. Use it in JSX: {claimType === "property" && <AmountField />}. Note: watch causes a re-render on every change — for performance, use getValues() in event handlers when you do not need reactivity.

🚨 Common Production Issues

**Bug: register spread on custom components** — {...register("field")} passes ref, which does not work on custom components. Use Controller for non-native inputs.

**Bug: reset() not working after async data load** — reset() must be called after the data is available. Use useEffect: useEffect(() => { if (data) reset(data); }, [data, reset]).

**Validation mode** — Default mode "onSubmit" means errors only show after the first submit attempt. For better UX, use mode: "onTouched" which validates on blur then on change.

✅ Best Practices

Always use Zod (or Yup) for schema validation — single source of truth for types and validation

Use mode: "onTouched" for better UX than the default "onSubmit" mode

Use setError for server-side validation errors returned from the API

Use reset() with defaultValues to pre-populate forms from API data

Use useFormContext for multi-step forms or deeply nested form components

Install React Hook Form DevTools in development for debugging

🏛️ Architect's Note

Senior Architect's Perspective

React Hook Form with Zod is the standard form solution for enterprise React in 2024. The combination gives you: type-safe form data (Zod infers TypeScript types from the schema), runtime validation, server error handling, and excellent performance. For multi-step forms, use useFormContext to share form state across step components without prop drilling. For very complex dynamic forms (field arrays, conditional schemas), RHF's useFieldArray handles arrays of fields cleanly.

💬 Frequently Asked Questions

How do I handle dynamic field arrays?

Use useFieldArray from RHF: const { fields, append, remove } = useFieldArray({ control, name: "items" }). This manages an array of form fields (e.g., multiple claimants, multiple line items) with add/remove functionality. Each field gets a stable id for the key prop.

How do I share form state across multiple components?

Use FormProvider and useFormContext. Wrap your form in <FormProvider {...methods}> and access the form methods in any child component with useFormContext<FormData>(). This is essential for multi-step forms where each step is a separate component.

🎯 Key Takeaways

1RHF uses uncontrolled inputs — no re-render on every keystroke
2register() for native inputs, Controller for third-party components
3Zod + zodResolver for type-safe schema validation
4handleSubmit validates before calling your submit function
5reset() to clear or pre-populate the form
6setError() for server-side validation errors
7useFormContext for sharing form state across components

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these React references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide React learning path

Browse the full React topic collection and learning order on SplashRide.

React Interview Prep

Practice React interview questions and scenario-based answers.

Frontend Engineer Career Path

A beginner-friendly journey through web foundations, React, production frontend engineering, and interview preparation.

Python Developer Career Path

A practical route through Python fundamentals, automation, backend basics, testing, projects, interview preparation, and career growth.

React Controlled Components: Inputs, Checkboxes, and Forms

Build React controlled inputs with value, checked, and onChange. Includes a complete form, reset behavior, number fields, and fixes for uncontrolled-input warnings.

State

Understand React state — the internal memory of a component that drives UI updates. Learn when to use local state, how to structure it, and the patterns senior engineers use to avoid the most common state bugs.