Всем привет! Думаю, что не ошибусь если скажу, что почти каждому фронтендеру приходится заниматься разработкой сложных форм. Те, кто уже имеют такой опыт знают, что работа с формами доставляет боль и страдания. Необходимо держать в голове все правила валидации и заполнения форм, связи между зависимыми полями, нужно как-то связывать данные формы с UI, при этом избегая лишних ререндеров.
На большом проекте мы писали формы через MobX + MVC, думаю, что это не самый плохой подход для написания форм, однако можно выделить следующие недостатки:
Многое пишем руками
Проверяем каждое поле самостоятельно, пишем логику ревалидации при изменении поля. Также для одного поля может быть несколько вариантов ошибок, а это означает, что нужно создавать под это массив ошибок, своевременно его обновлять и поддерживать актуальность
Лишний код
К пункту выше добавляются еще бесполезные методы в Controller для изменения каждого поля, например:
// SomeController.tsx export class SomeController { ... public setDuration = (v: number) => { this.store.duration = v; } }
Приходится писать много "мусорного" кода, который не является бизнес логикой. Нужно добавить одно новое поле ? Не забудь создать под него кучу других полей и методов, которые будут хранить и обрабатывать "мета" информацию.
Вот здесь в игру вступает React Hook Form + Zod. С помощью этих библиотек можно описывать формы декларативно.
Пример описания формы (RHF + Zod):
import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const formSchema = z.object({ name: z.string().min(1, 'Введите имя'), email: z.string().email('Некорректный email'), age: z.coerce.number().min(0, 'Некорректный возраст').optional(), }); type FormValues = z.infer<typeof formSchema>; export function ExampleForm() { const { register, handleSubmit, formState: { errors }, } = useForm<FormValues>({ resolver: zodResolver(formSchema), defaultValues: { name: '', email: '', }, }); return ( <form onSubmit={handleSubmit((data) => console.log(data))}> <div> <input {...register('name')} /> {errors.name && <span>{errors.name.message}</span>} </div> <div> <input type="email" {...register('email')} /> {errors.email && <span>{errors.email.message}</span>} </div> <div> <input type="number" {...register('age')} /> {errors.age && <span>{errors.age.message}</span>} </div> <button type="submit">Отправить</button> </form> ); }
Когда мы на проекте написали новый раздел с использованием этих иструментов, то были приятно удивлены меньшим количеством кода и простотой.
Не все так гладко
С момента внедрения React Hook Form в наш проект прошел уже год, могу сказать что теперь почти все формы в продукте используют данный подход, это удобно и новые изменения вносятся довольно просто. Однако за это время обнаружился ряд недостатков.
1. Проблемы с undefined полями.
Описывая структуру своей формы, вы закладываете все возможные значения для конкретного поля.
const schema = z.object({ age: z.number() }); // Меняем значение поля methods.setValue('age', undefined)
Вполне логичный код, да вот только такой прием не сработает. В документации это описано:
The value for the field. This argument is required and can not be
undefined.
Однако это не очевидно. Поискав issues на эту тему вы найдете много таких, например вот или вот. Множество людей так же как и я столкнулись с подобной проблемой, и в этой ситуации первое решение, которое приходит на ум следующее:
const schema = z.object({ age: z.number().nullable() }); // Меняем значение поля methods.setValue('age', null)
Как по мне, это выглядит костыльно и в нашем случае в добавок ломает китовый компонент ввода возраста, который не ожидает, что в него будут передавать null. Иногда бывает и наоборот - китовый компонент отдает undefined значение при очистке и установка значения не происходит.
Недавно мне поступило требование, добавить кнопку полной очистки формы к пустым полям. Сначала это показалось простым - добавляем кнопку и на onClick вешаем methods.reset(), это должно сбросить форму к default значениям, которые мы описали при создании формы:
const methods = useForm<FormValues>({ resolver: zodResolver(formSchema), defaultValues: { name: undefined, email: undefined, }, });
Но тут опять же возникает проблема с установкой поле в undefined значения. Самое простое, что можно сделать, это опять же создать поля .nullable() и описывать дефолтные значения так:
const methods = useForm<FormValues>({ resolver: zodResolver(formSchema), defaultValues: { name: null, email: null, }, });
Если ваше поле строка, то можно обойтись дефолтным значением не null а '', это тоже будет работать.
2. Неожиданные ошибки от Zod
Это скорее не проблема React Hook Form напрямую. а проблема в связке с Zod. Например вы описали схему вида:
const schema = z.object({ field1: z.string().min(1, 'Заполни поле').optional() });
Далее ваш компонент при очистке отдает null, готовьтесь ловить в интерфейсе неприятную ошибку invalid type (expected string received null).
Вашим пользователям это явно не понравится. Чтобы таких стуаций не возникало необходимо явно указывать один и тот же текст для всех возможных типов ошибок:
// zod v4 const schema = z.object({ field1: z.string('Заполни поле').min(1, 'Заполни поле').optional() });
3. Магический isDirty
Флаг, получаемый из methods.formState.isDirty, показывает есть ли изменения на вашей форме. Это полезно для ситуаций когда пользователь заполнил форму и пытается уйти со страницы, в таком случаем по этому флагу мы можете показать пользователю предупреждение. Однако будьте осторожны с таким кейсом:
const formSchema = z.object({ name: z.string().min(1, 'Введите имя'), age: z.coerce.number().min(0, 'Некорректный возраст').optional(), }); export function ExampleForm() { const methods = useForm({ resolver: zodResolver(formSchema), defaultValues: {}, }); // На первый рендер будет true, хотя еще ничего не меняли! const isDirty = methods.formState.isDirty; // Здесь будет пустой объект {} const dirtyFields = methods.formState.dirtyFields; return ...; }
Это кажется странным, все из-за того что мы передали в defaultValues пустой объект.
Вывод
Сложные формы по-прежнему отнимают много внимания, но инструменты реально могут снять с команды лишний шаблонный код. Переход с ручной логики в стиле MobX и MVC на декларативное описание через React Hook Form и Zod у нас окупился: формы проще читать и менять.
За год в проде всплывали неожиданности, без них не обошлось. В целом новый подход мы не жалеем и продолжаем на нём строить интерфейсы. Главный итог простой: выбирайте стек, который уменьшает рутину, но оставайтесь внимательными к деталям реализации — тогда и скорость разработки, и поддержка останутся на приемлемом уровне.
Комментарии (2)

CzarOfScripts
19.04.2026 18:35Читая такие статьи о зоде, понимаешь, что человек скорее всего первый раз на простеньком примере попробовал его и все, а потом вылазит кучу тонкостей. Например тот самый infer который используют все, но мало кто знает про input/output, из-за чего в резолвере для формика вылазят ошибки, когда передаешь одни значения, но схема на выходе выдает далеко не принимаемые типы и ты ловишь кучу ошибок и не понимаешь в чем дело, в зоде? В формике или еще чем-то?
Ksen077
Спасибо, что подсветили такие тонкие моменты, тоже хотим перейти на RHF в своем проекте.