- PVSM.RU - https://www.pvsm.ru -
Всем привет! Думаю, что не ошибусь если скажу, что почти каждому фронтендеру приходится заниматься разработкой сложных форм. Те, кто уже имеют такой опыт знают, что работа с формами доставляет боль и страдания. Необходимо держать в голове все правила валидации и заполнения форм, связи между зависимыми полями, нужно как-то связывать данные формы с 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 на эту тему вы найдете много таких, например вот [1] или вот [2]. Множество людей так же как и я столкнулись с подобной проблемой, и в этой ситуации первое решение, которое приходит на ум следующее:
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 у нас окупился: формы проще читать и менять.
За год в проде всплывали неожиданности, без них не обошлось. В целом новый подход мы не жалеем и продолжаем на нём строить интерфейсы. Главный итог простой: выбирайте стек, который уменьшает рутину, но оставайтесь внимательными к деталям реализации — тогда и скорость разработки, и поддержка останутся на приемлемом уровне.
Автор: alexkomarchev
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/449976
Ссылки в тексте:
[1] вот: https://github.com/react-hook-form/react-hook-form/issues/12277
[2] вот: https://github.com/orgs/react-hook-form/discussions/5858
[3] Источник: https://habr.com/ru/articles/1025218/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1025218
Нажмите здесь для печати.