Назад к блогу
Frontend

React Hook Form + Zod: формы и валидация

Создание форм с React Hook Form и валидацией Zod: schema, обработка ошибок, контролируемые поля и лучшие практики.

5 января 2026 г.
10 мин чтения
253 просмотров
MOLOTILO

MOLOTILO DIGITAL

React Hook Form + Zod: формы и валидация

Почему React Hook Form

React Hook Form — библиотека для работы с формами, которая минимизирует ререндеры и упрощает валидацию. В сочетании с Zod получается мощная типобезопасная система форм.

Базовая форма

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

// Zod schema
const loginSchema = z.object({
  email: z.string().email('Введите корректный email'),
  password: z.string().min(8, 'Минимум 8 символов'),
  rememberMe: z.boolean().optional()
});

type LoginFormData = z.infer<typeof loginSchema>;

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: '',
      password: '',
      rememberMe: false
    }
  });

  const onSubmit = async (data: LoginFormData) => {
    console.log(data);
    // API запрос
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          {...register('email')}
          type="email"
          placeholder="Email"
        />
        {errors.email && (
          <span className="error">{errors.email.message}</span>
        )}
      </div>

      <div>
        <input
          {...register('password')}
          type="password"
          placeholder="Пароль"
        />
        {errors.password && (
          <span className="error">{errors.password.message}</span>
        )}
      </div>

      <label>
        <input {...register('rememberMe')} type="checkbox" />
        Запомнить меня
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Вход...' : 'Войти'}
      </button>
    </form>
  );
}

Сложные Zod схемы

// Регистрация с подтверждением пароля
const registerSchema = z.object({
  name: z.string().min(2, 'Минимум 2 символа'),
  email: z.string().email('Некорректный email'),
  password: z
    .string()
    .min(8, 'Минимум 8 символов')
    .regex(/[A-Z]/, 'Нужна заглавная буква')
    .regex(/[0-9]/, 'Нужна цифра'),
  confirmPassword: z.string(),
  age: z.number().min(18, 'Минимум 18 лет').optional(),
  role: z.enum(['user', 'admin']),
  terms: z.literal(true, {
    errorMap: () => ({ message: 'Примите условия' })
  })
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Пароли не совпадают',
  path: ['confirmPassword']
});

// Условная валидация
const orderSchema = z.object({
  deliveryType: z.enum(['pickup', 'delivery']),
  address: z.string().optional()
}).refine((data) => {
  if (data.deliveryType === 'delivery') {
    return data.address && data.address.length > 5;
  }
  return true;
}, {
  message: 'Укажите адрес доставки',
  path: ['address']
});

// Массивы и вложенные объекты
const productSchema = z.object({
  name: z.string().min(1),
  variants: z.array(z.object({
    size: z.string(),
    price: z.number().positive(),
    stock: z.number().int().min(0)
  })).min(1, 'Добавьте хотя бы один вариант')
});

Контролируемые поля

import { useForm, Controller } from 'react-hook-form';
import { Select, DatePicker, Slider } from './ui';

function ProductForm() {
  const { control, handleSubmit } = useForm<ProductFormData>({
    resolver: zodResolver(productSchema)
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Кастомный Select */}
      <Controller
        name="category"
        control={control}
        render={({ field, fieldState }) => (
          <Select
            {...field}
            options={categories}
            error={fieldState.error?.message}
          />
        )}
      />

      {/* DatePicker */}
      <Controller
        name="publishDate"
        control={control}
        render={({ field }) => (
          <DatePicker
            selected={field.value}
            onChange={field.onChange}
          />
        )}
      />

      {/* Slider */}
      <Controller
        name="price"
        control={control}
        render={({ field }) => (
          <Slider
            value={field.value}
            onChange={field.onChange}
            min={0}
            max={10000}
          />
        )}
      />
    </form>
  );
}

Динамические поля (useFieldArray)

import { useForm, useFieldArray } from 'react-hook-form';

function OrderForm() {
  const { register, control, handleSubmit } = useForm<OrderFormData>({
    defaultValues: {
      items: [{ name: '', quantity: 1 }]
    }
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'items'
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`items.${index}.name`)} />
          <input
            {...register(`items.${index}.quantity`, { valueAsNumber: true })}
            type="number"
          />
          <button type="button" onClick={() => remove(index)}>
            Удалить
          </button>
        </div>
      ))}

      <button type="button" onClick={() => append({ name: '', quantity: 1 })}>
        Добавить товар
      </button>

      <button type="submit">Оформить заказ</button>
    </form>
  );
}

Обработка ошибок сервера

function RegistrationForm() {
  const { setError, handleSubmit } = useForm<FormData>({
    resolver: zodResolver(schema)
  });

  const onSubmit = async (data: FormData) => {
    try {
      await registerUser(data);
    } catch (error) {
      if (error.code === 'EMAIL_EXISTS') {
        setError('email', {
          type: 'server',
          message: 'Email уже зарегистрирован'
        });
      } else {
        setError('root', {
          type: 'server',
          message: 'Произошла ошибка, попробуйте позже'
        });
      }
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {errors.root && <div className="error">{errors.root.message}</div>}
      {/* поля формы */}
    </form>
  );
}

Заключение

React Hook Form + Zod — мощная комбинация для типобезопасных форм. Минимум ререндеров, декларативная валидация, отличная поддержка TypeScript. Используйте Controller для кастомных компонентов, useFieldArray для динамических полей.

Валидация на клиенте — для UX, валидация на сервере — для безопасности. Используйте обе.

ReactФормыВалидацияZodReact Hook Form

Понравилась статья?

Подпишитесь на наш блог, чтобы не пропустить новые материалы