Валидация форм в React

Валидация форм в React строится вокруг управления состоянием полей, обработки пользовательского ввода и проверки данных до их отправки на сервер. При усложнении логики форм ручная проверка быстро становится громоздкой: появляются дублирующиеся условия, разрозненные проверки и сложность синхронизации типов данных между фронтендом и API. Использование схем валидации позволяет централизовать правила и обеспечить предсказуемость структуры данных.

Библиотека Zod предоставляет декларативный подход к описанию схем данных с полной поддержкой TypeScript. Основная идея заключается в том, что структура данных описывается один раз и затем используется как для проверки значений, так и для вывода типов.

В основе Zod лежат схемы, описывающие ожидаемую структуру объекта. Каждое поле получает набор ограничений, которые проверяются во время выполнения.

import { z } from "zod";

const userSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().positive()
});

Схема определяет контракт данных. При попытке валидации входящего объекта происходит проверка всех полей:

const result = userSchema.safeParse({
  name: "Alex",
  email: "alex@mail.com",
  age: 25
});

Метод safeParse возвращает объект с результатом, не выбрасывая исключений. Это важно для React-приложений, где ошибки валидации должны управляться через состояние интерфейса.

Интеграция с React

В React формы обычно управляются через локальное состояние или специализированные библиотеки. В связке с Zod часто используется React вместе с контроллерами форм, такими как react-hook-form.

Схема становится источником истины, а React-компонент отвечает только за отображение состояния.

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

const form = useForm({
  resolver: zodResolver(userSchema)
});

В этом подходе валидация отделяется от UI-логики. React получает уже проверенные данные или список ошибок, привязанных к конкретным полям.

Привязка ошибок к полям формы

Результат валидации Zod структурирован. Ошибки можно сопоставить с конкретными полями формы:

const { errors } = form.formState;

console.log(errors.name?.message);

Каждое поле содержит сообщение об ошибке, сформированное на основе правил схемы. Это позволяет унифицировать обработку ошибок без ручного написания условий в компонентах.

Типизация данных

Одним из ключевых преимуществ Zod является интеграция с TypeScript. Типы выводятся автоматически из схемы:

type User = z.infer<typeof userSchema>;

Это устраняет дублирование типов между фронтендом и схемой валидации. Любое изменение схемы автоматически отражается в типах данных.

В контексте форм это означает, что значения формы и валидированные данные всегда синхронизированы по структуре.

Кастомные правила валидации

Помимо стандартных методов проверки, Zod поддерживает пользовательские условия через refine:

const passwordSchema = z.string().min(8).refine((val) => {
  return /[A-Z]/.test(val) && /[0-9]/.test(val);
}, {
  message: "Пароль должен содержать заглавную букву и цифру"
});

Такой подход позволяет реализовывать бизнес-логику, не выходя за пределы схемы.

В React это особенно важно, поскольку логика проверки становится частью декларативного описания данных, а не компонента.

Составные схемы и переиспользование

Схемы могут быть композиционными. Это позволяет строить сложные формы из повторно используемых блоков:

const addressSchema = z.object({
  city: z.string(),
  street: z.string(),
  zip: z.string()
});

const userSchema = z.object({
  name: z.string(),
  address: addressSchema
});

Такая структура снижает дублирование и упрощает поддержку больших форм, где одни и те же блоки используются в разных контекстах.

Условная валидация

Сложные формы часто требуют динамических правил. Zod поддерживает условную логику через superRefine:

const schema = z.object({
  password: z.string(),
  confirmPassword: z.string()
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      path: ["confirmPassword"],
      message: "Пароли не совпадают",
      code: "custom"
    });
  }
});

Это позволяет описывать зависимости между полями без нарушения структуры схемы.

Асинхронная валидация

Некоторые проверки требуют обращения к серверу, например проверка уникальности email. Zod поддерживает асинхронные схемы:

const emailSchema = z.string().email().refine(async (email) => {
  const res = await fetch(`/api/check-email?email=${email}`);
  const data = await res.json();
  return data.available;
}, {
  message: "Email уже используется"
});

В React такие проверки обычно комбинируются с debounce, чтобы снизить нагрузку на сервер и не выполнять запрос при каждом изменении поля.

Обработка массивов и динамических форм

Формы часто содержат списки элементов, например набор телефонов или адресов. Zod предоставляет инструменты для работы с массивами:

const schema = z.object({
  phones: z.array(z.string().min(10))
});

В React это используется для динамического добавления и удаления полей без изменения общей структуры формы.

Преобразование данных

Zod позволяет трансформировать данные в процессе валидации:

const schema = z.object({
  age: z.string().transform((val) => Number(val))
});

Это особенно полезно при работе с HTML-формами, где все значения приходят в виде строк.

Работа с необязательными полями

Формы часто содержат поля, которые могут отсутствовать. Zod явно поддерживает такие случаи:

const schema = z.object({
  middleName: z.string().optional()
});

Отсутствие значения не приводит к ошибке, что упрощает обработку частично заполненных форм.

Интеграция с UI-состоянием

В React-архитектуре валидация через Zod часто связывается с состояниями интерфейса: loading, error, success. Схема выступает источником правил, а UI реагирует на результат проверки.

Ошибки могут отображаться сразу после изменения поля или только при отправке формы. Поведение определяется уровнем интеграции с менеджером формы.

Централизация бизнес-логики

Использование схем валидации переносит часть бизнес-логики из компонентов в отдельный слой. Это снижает связность и делает код предсказуемым при масштабировании.

Схема становится универсальным контрактом между интерфейсом, сервером и типами данных.