React
Формы с React Hook Form и Zod
Управляемая форма на десять полей это десять useState и десять onChange, каждый из которых ре-рендерит весь компонент формы на каждую нажатую клавишу. Валидация расползается по обработчикам, типы значений приходится держать в голове, а схему проверки на бэкенде пишут заново и независимо - и она тихо расходится с фронтом. React Hook Form и Zod закрывают обе боли разом: поля становятся неуправляемыми и не ре-рендерят форму на каждый ввод, а одна Zod-схема даёт и валидацию, и тип, и общий контракт с бэком.
- Формы регистрации, оплаты и онбординга, где важна и скорость ввода, и строгая валидация перед отправкой
- Сложные формы с условными полями, массивами и вложенными секциями, где ручной useState не масштабируется
- Полный стек на TypeScript: одна Zod-схема валидирует и в браузере, и в API-обработчике на сервере
- Командные кодовые базы, где тип данных формы выводится из схемы, а не дублируется руками
- Проекты с запретом any и кастов, где типобезопасность формы обеспечивается выводом из Zod
Предварительные знания
- Управляемые поля: value плюс onChange и хранение в useState
- Базовый TypeScript: интерфейсы, дженерики и вывод типов
- Идея валидации данных перед отправкой на сервер
Зачем понадобились неуправляемые формы и схемы
К концу 2010-х управляемые формы стали узким местом производительности: каждое нажатие клавиши обновляло состояние и перерисовывало форму целиком. React Hook Form (Билл Луо, 2019) вернулся к неуправляемым полям, читая значения из DOM через ref и сводя ре-рендеры к минимуму. Параллельно Колин Макдоннелл выпустил Zod (2020) - схему валидации, которая одновременно выводит статический тип данных. Их связка через zodResolver дала редкое сочетание: одна декларация описывает и проверку в рантайме, и тип на этапе компиляции, и контракт между клиентом и сервером.
React Hook Form: неуправляемые поля и register
В управляемой форме каждое поле хранится в useState, и любой ввод обновляет состояние, перерисовывая компонент формы целиком. На большой форме это заметно: десятки ре-рендеров на каждое нажатие. React Hook Form идёт другим путём - поля остаются неуправляемыми, а их значения читаются напрямую из DOM по ссылке. Функция register подключает input к форме, отдавая ему ref и обработчики.
handleSubmit оборачивает обработчик: он сначала прогоняет валидацию и вызывает onSubmit только с корректными данными. formState несёт ошибки, флаги isSubmitting и isValid. Ввод текста не вызывает ре-рендер всей формы - перерисовываются лишь те части, что подписаны на конкретные ошибки или значения.
- Управляемая форма — value плюс onChange, состояние в useState. Ре-рендер компонента на каждое нажатие клавиши. Простая, но не масштабируется по производительности.
- React Hook Form — Неуправляемые поля через register, значения из DOM по ref. Минимум ре-рендеров. Масштабируется на большие формы.
Для интеграции со сторонними управляемыми UI-компонентами (кастомные селекты, дейтпикеры) используется Controller - мост, который оборачивает управляемое поле в модель RHF, сохраняя единый источник значений и ошибок.
Почему React Hook Form вызывает меньше ре-рендеров, чем управляемая форма?
Zod: схема, которая задаёт и валидацию, и тип
Zod это библиотека схем валидации, у которой есть особое свойство: из схемы выводится статический TypeScript-тип. Схема описывается один раз, а тип получается через z.infer без дублирования. Это снимает классическое расхождение, когда интерфейс данных и правила проверки живут отдельно и со временем перестают совпадать.
Метод parse выбрасывает ошибку на некорректных данных, а safeParse возвращает размеченный результат { success, data } или { success, error }. Это дискриминированное объединение: после проверки success TypeScript сам сужает тип, и доступ к data типобезопасен без всякого приведения. Никаких as и non-null assertion не требуется - тип сужается самим языком.
Соблазн написать input as Signup убирает ровно ту проверку, ради которой нужна схема: каст обманывает компилятор, но не проверяет данные в рантайме. Правильный путь это safeParse плюс проверка success, после которой тип сужается сам. Так данные и проверены, и типизированы без any и без каста.
В чём ключевое преимущество описания данных формы Zod-схемой вместо отдельного интерфейса?
zodResolver и одна схема на фронт и бэк
zodResolver соединяет React Hook Form и Zod. Схема передаётся в useForm как resolver, и RHF использует её и для валидации полей, и для вывода типа значений формы. Тип формы при этом берётся из z.infer той же схемы, так что register, handleSubmit и ошибки полностью типизированы без единого ручного интерфейса.
Самая ценная выгода в том, что та же схема живёт и на сервере. API-обработчик импортирует signupSchema и прогоняет тело запроса через safeParse, прежде чем что-либо делать. Клиент и сервер делят один контракт: правила валидации и тип определены в одном месте, и расхождению взяться неоткуда.
Тело запроса осознанно типизируется как unknown, а не приводится кастом. Единственный честный способ перейти от unknown к типизированным данным это валидация: safeParse проверяет форму данных в рантайме и одновременно сужает тип. Это в точности то, что заменяет небезопасный as.
Какую главную выгоду даёт переиспользование одной Zod-схемы на клиенте и на сервере?
Связь с другими темами
Этот урок про формы и валидацию. Рядом стоят соседние темы:
- Управляемые формы — Базовый подход с value и onChange, относительно которого RHF выигрывает на производительности и объёме кода
- TypeScript и React — Вывод типа формы из Zod-схемы это конкретный пример строгой типизации без any, кастов и non-null assertion
Итог
- React Hook Form держит поля неуправляемыми и читает значения через register, поэтому ввод не ре-рендерит форму на каждую клавишу
- Zod описывает схему валидации, которая одновременно выводит статический тип данных через z.infer
- zodResolver соединяет RHF и Zod: схема становится единственным источником и проверки, и ошибок формы
- Одна и та же Zod-схема валидирует данные и в браузере, и в API-обработчике на сервере - общий контракт без дублирования
- Тип формы выводится из схемы, а не пишется руками, что исключает расхождение типа и валидации
- Типобезопасность достигается выводом и type guards, без any, без non-null assertion и без явных кастов
Связанные уроки
- rc-08-forms-controlled — Управляемые поля задают базовую модель форм, на фоне которой видна выгода неуправляемого подхода RHF
- rc-41-typescript-react — Вывод типа формы из Zod-схемы это прикладной пример строгой типизации без any и кастов