Formik и Zod

Современные формы в веб-приложениях требуют сочетания управляемого состояния, декларативной валидации и строгой типизации. Комбинация Formik и Zod формирует устойчивую архитектуру, в которой ответственность разделяется между управлением формой и описанием правил данных.

Formik обеспечивает жизненный цикл формы: хранение значений, отслеживание touched/dirty состояний, обработку сабмита и централизованное управление ошибками. Zod выступает как слой валидации и типового контракта, задающий структуру данных независимо от UI-логики.


Роль Formik в управлении формами

Formik строится вокруг концепции контролируемых форм. Состояние формы хранится в едином объекте, а изменения происходят через явные обработчики.

Ключевые возможности:

  • централизованное состояние значений (values)
  • управление ошибками (errors)
  • отслеживание взаимодействия (touched)
  • обработка отправки (handleSubmit)
  • интеграция с React через hooks или HOC

Базовая модель Formik выглядит как слой, абстрагирующий работу с input-элементами:

const formik = useFormik({
  initialValues: {
    email: '',
    password: ''
  },
  onSubmit: (values) => {
    console.log(values);
  }
});

Formik не накладывает строгих правил на структуру данных, поэтому валидация выносится во внешние схемы.


Роль Zod как слоя описания данных

Zod используется как декларативный инструмент описания формы и проверки входных данных. В отличие от ручных валидаторов, схема становится источником истины для структуры объекта.

Пример базовой схемы:

import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});

Сильная сторона Zod заключается в том, что схема одновременно:

  • валидирует данные во время выполнения
  • выводит TypeScript-типы
  • может быть переиспользована на клиенте и сервере

Связка Formik и Zod через адаптацию схемы

Formik ожидает функцию валидации или schema-адаптер (например, Yup). Zod не интегрирован напрямую, поэтому создаётся промежуточный слой преобразования.

Типовой подход — функция адаптации:

const validateWithZod = (schema) => (values) => {
  const result = schema.safeParse(values);

  if (result.success) return {};

  const errors = {};
  result.error.issues.forEach((issue) => {
    errors[issue.path[0]] = issue.message;
  });

  return errors;
};

Использование в Formik:

const formik = useFormik({
  initialValues: {
    email: '',
    password: ''
  },
  validate: validateWithZod(schema),
  onSubmit: (values) => {
    console.log(values);
  }
});

Такой подход сохраняет независимость Formik от Zod и обеспечивает явное преобразование ошибок.


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

При увеличении количества полей схемы Zod становятся вложенными и композиционными.

Пример вложенной структуры:

const profileSchema = z.object({
  name: z.string().min(2),
  contacts: z.object({
    email: z.string().email(),
    phone: z.string().optional()
  })
});

Formik в этом случае хранит значения в аналогичной структуре:

initialValues: {
  name: '',
  contacts: {
    email: '',
    phone: ''
  }
}

Ключевым моментом становится корректное отображение путей ошибок.


Преобразование ошибок Zod в структуру Formik

Zod возвращает ошибки в виде массива issues, где каждый элемент содержит путь (path) и сообщение.

Для корректной интеграции требуется рекурсивная сборка объекта ошибок:

const buildErrors = (issues) => {
  return issues.reduce((acc, issue) => {
    const path = issue.path.join('.');
    acc[path] = issue.message;
    return acc;
  }, {});
};

Это позволяет поддерживать вложенные поля Formik:

  • contacts.email
  • contacts.phone

Formik автоматически сопоставляет такие ключи с соответствующими полями.


Типизация форм через Zod

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

type FormValues = z.infer<typeof schema>;

Использование в Formik:

const formik = useFormik<FormValues>({
  initialValues: {
    email: '',
    password: ''
  },
  validate: validateWithZod(schema),
  onSubmit: (values) => {}
});

Это устраняет дублирование типов и схем.


Асинхронная валидация и серверные ошибки

Formik поддерживает асинхронную валидацию через validate или validationSchema. В связке с Zod асинхронность чаще реализуется отдельно от схемы.

Сценарий серверной проверки:

const submitHandler = async (values, { setErrors }) => {
  const response = await api.check(values);

  if (!response.ok) {
    setErrors(response.errors);
    return;
  }
};

Zod используется только для синхронной структуры, а серверная логика дополняет её.


Композиция схем и переиспользование логики

Zod поддерживает композицию через merge, extend и partial.

Пример расширения:

const baseSchema = z.object({
  email: z.string().email()
});

const extendedSchema = baseSchema.extend({
  password: z.string().min(8)
});

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

Formik при этом остаётся неизменным, так как работает только с итоговой схемой.


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

Zod поддерживает условные правила через refine:

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

Formik получает ошибку как часть стандартного механизма и отображает её в поле confirmPassword.


Оптимизация повторных рендеров

Formik может вызывать частые обновления при изменении значений. При использовании Zod важно учитывать стоимость валидации.

Подходы оптимизации:

  • дебаунс валидации
  • валидация только на submit
  • мемоизация схемы
  • разделение формы на подформы

Пример отложенной валидации:

const debouncedValidate = debounce((values) => {
  return validateWithZod(schema)(values);
}, 300);

Разделение ответственности между слоями

Архитектурно связка Formik + Zod формирует три уровня:

  • UI слой (React компоненты)
  • слой управления состоянием (Formik)
  • слой бизнес-валидации (Zod)

Такое разделение позволяет:

  • переносить схемы между фронтендом и бэкендом
  • централизовать правила данных
  • минимизировать дублирование логики

Использование схем как контракта API формы

Zod-схема часто становится контрактом между интерфейсом и сервером.

Пример:

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});

Эта схема используется:

  • в Formik для валидации UI
  • на сервере для проверки payload
  • в TypeScript для типизации DTO

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

Formik поддерживает FieldArray, а Zod описывает массивы через z.array.

const schema = z.object({
  tags: z.array(z.string().min(1))
});

Formik:

  • добавляет элементы через push
  • удаляет через remove

Ошибки Zod приходят с индексами:

  • tags[0]
  • tags[1]

И требуют нормализации для Formik.


Практическая модель интеграции

Обобщённая структура интеграции выглядит следующим образом:

  • схема Zod определяет форму
  • TypeScript выводит тип
  • Formik управляет состоянием
  • адаптер преобразует ошибки
  • UI отображает состояние полей

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