Back to Rules
React96% popularity

React Form Handling with React Hook Form

Build performant forms with React Hook Form using uncontrolled components and validation.

Beier(Bill) LuoUpdated Mar 22, 2024

Overview

React Hook Form brings performance to form handling by using uncontrolled components with ref tracking rather than controlled components that maintain state on every keystroke. This approach dramatically reduces re-renders and enables form state management without performance overhead. The register function connects form inputs to the form state without wrapping inputs in providers or context. It returns onChange, onBlur, name, and ref properties that wire directly to HTML elements. This minimal API surface keeps bundle size small while providing comprehensive form functionality. Validation integrates with popular validation libraries through resolver patterns. Zod validation provides type-safe schemas that automatically infer TypeScript types for form data. This eliminates the manual type annotation step and ensures validation rules and types stay in sync as requirements change. Form state accessed through useForm returns methods for triggering validation, errors, and touched fields. The handleSubmit callback receives only valid form data when validation passes. The shouldUseNativeValidation prop enables browser-native validation when custom validation isn't required, reducing JavaScript bundle impact. Error display should react to the form state rather than local component state. Using Controller component for complex inputs like date pickers or dropdowns wraps controlled components in the uncontrolled pattern. This enables custom inputs to participate in form validation without managing their own form state.

Code Example

.reactrules
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';

const projectSchema = z.object({
  name: z.string()
    .min(3, 'Name must be at least 3 characters')
    .max(50, 'Name cannot exceed 50 characters'),
  description: z.string()
    .max(500, 'Description cannot exceed 500 characters')
    .optional(),
  startDate: z.date({
    required_error: 'Start date is required',
  }),
  endDate: z.date().optional(),
  teamMembers: z.array(z.object({
    userId: z.string(),
    role: z.enum(['admin', 'member', 'viewer']),
  })).min(1, 'At least one team member is required'),
  tags: z.array(z.string()).max(5, 'Maximum 5 tags allowed'),
  isPublic: z.boolean().default(false),
}).refine(
  (data) => !data.endDate || data.endDate > data.startDate,
  { message: 'End date must be after start date', path: ['endDate'] }
);

type ProjectFormData = z.infer<typeof projectSchema>;

export function CreateProjectForm({ onSubmit, defaultValues }: {
  onSubmit: (data: ProjectFormData) => void;
  defaultValues?: Partial<ProjectFormData>;
}) {
  const {
    register,
    handleSubmit,
    control,
    watch,
    setValue,
    formState: { errors, isSubmitting, isValid },
  } = useForm<ProjectFormData>({
    resolver: zodResolver(projectSchema),
    defaultValues: {
      isPublic: false,
      tags: [],
      ...defaultValues,
    },
    mode: 'onBlur',
  });

  const watchedFields = watch(['name', 'tags']);

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      <div>
        <label htmlFor="name">Project Name</label>
        <input
          id="name"
          {...register('name')}
          placeholder="My Awesome Project"
        />
        {errors.name && (
          <span className="error">{errors.name.message}</span>
        )}
        <span className="char-count">
          {watchedFields[0]?.length || 0}/50
        </span>
      </div>

      <div>
        <label>Description</label>
        <textarea
          {...register('description')}
          rows={4}
          placeholder="What does this project do?"
        />
        {errors.description && (
          <span className="error">{errors.description.message}</span>
        )}
      </div>

      <div className="date-range">
        <div>
          <label>Start Date</label>
          <Controller
            name="startDate"
            control={control}
            render={({ field }) => (
              <DatePicker
                selected={field.value}
                onChange={field.onChange}
                dateFormat="yyyy-MM-dd"
              />
            )}
          />
          {errors.startDate && (
            <span className="error">{errors.startDate.message}</span>
          )}
        </div>

        <div>
          <label>End Date (Optional)</label>
          <Controller
            name="endDate"
            control={control}
            render={({ field }) => (
              <DatePicker
                selected={field.value}
                onChange={field.onChange}
                dateFormat="yyyy-MM-dd"
                minDate={watch('startDate')}
              />
            )}
          />
          {errors.endDate && (
            <span className="error">{errors.endDate.message}</span>
          )}
        </div>
      </div>

      <div>
        <label>
          <input type="checkbox" {...register('isPublic')} />
          Make project public
        </label>
      </div>

      <div className="form-actions">
        <button
          type="button"
          onClick={() => setValue('name', '')}
        >
          Reset
        </button>
        <button
          type="submit"
          disabled={isSubmitting || !isValid}
        >
          {isSubmitting ? 'Creating...' : 'Create Project'}
        </button>
      </div>
    </form>
  );
}

More React Rules

REACT
94%

React Component Testing with Testing Library

Test behavior over implementation. Use Testing Library queries to interact with components as users would.

testingreacttesting-library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm }...
Jan 28, 2024by Kent C. Dodds
View Rule
REACT
96%

React Server Components Data Fetching Patterns

Master async/await patterns in Server Components for efficient data loading and caching.

reactserver-componentsdata-fetching
import { db } from '@/lib/db';
import { cache } from 'next/cache';
import { Suspense } from 'react';
import { Skeleton } from '@/components/ui/skeleto...
Feb 10, 2024by React Team
View Rule
REACT
95%

React Performance Optimization Patterns

Use React.memo, useMemo, and useCallback correctly to prevent unnecessary re-renders.

reactperformanceoptimization
import React, { useState, useMemo, useCallback, memo, useEffect } from 'react';

// Expensive calculation helper
function calculateExpensiveValue(item...
Jan 25, 2024by Dan Abramov
View Rule