Формы без боли: react-hook-form + zod = схема в центре, минимум ререндеров — 30 июня 2026 г. в 06:01:23.299
Формы без боли: react-hook-form + zod = схема в центре, минимум ререндеров Сейчас покажу, как я собираю формы так, чтобы валидация была в одном месте, а компоненты не прыгали от каждого ввода. 1) Схема — источник правды import { z } from 'zod'; export const userSchema = z.object({ email: z.string().email(), age: z.number().int().min(18), newsletter: z.boolean().default(false), }); export type UserForm = z.infer<typeof userSchema>; 2) Инициализация формы с резолвером import { useForm, FormProvider } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; const UserForm = ({ initial }: { initial?: Partial<UserForm> }) => { const methods = useForm<UserForm>({ resolver: zodResolver(userSchema), defaultValues: { newsletter: false, ...initial }, mode: 'onChange', // мгновенная подсветка ошибок }); const { handleSubmit, formState: { isSubmitting, errors } } = methods; const onSubmit = async (data: UserForm) => { // маппим серверные ошибки обратно в форму при необходимости // setError('email', { type: 'server', message: 'уже занято' }) await api.saveUser(data); }; return ( <FormProvider {...methods}> <form onSubmit={handleSubmit(onSubmit)}> <Input name="email" label="Email" /> <NumberInput name="age" label="Возраст" /> <Checkbox name="newsletter" label="Подписаться" /> {errors.root && <p className="error">{errors.root.message}</p>} <button disabled={isSubmitting}>Сохранить</button> </form> </FormProvider> ); }; 3) Поля - через контекст, с мемоизацией import { useFormContext, Controller } from 'react-hook-form'; // Простой контролируемый инпут с минимальными ререндерами export const Input = React.memo(({ name, label }: { name: string; label: string }) => { const { register, formState: { errors } } = useFormContext(); return ( <label> {label} <input {...register(name)} /> {errors[name] && <span className="error">{String(errors[name]?.message)}</span>} </label> ); }); // Пример для нестандартного компонента через Controller export const NumberInput = React.memo(({ name, label }: { name: string; label: string }) => { const { control, formState: { errors } } = useFormContext(); return ( <label> {label} <Controller control={control} name={name} render={({ field }) => <input type="number" {...field} />} /> {errors[name] && <span className="error">{String(errors[name]?.message)}</span>} </label> ); }); 4) Динамические списки - useFieldArray const { control } = useFormContext(); const { fields, append, remove } = useFieldArray({ control, name: 'phones' }); Храните массивы в форме, рендерите по fields, добавляйте/удаляйте кнопками - ререндерится только нужный участок. 5) Практические советы - Включайте DevTools формы только в dev. - Ошибки сервера маппьте через setError, а не кидайте alert. - При редактировании сущности меняйте key формы (<form key={user.id}>) — проще, чем делать reset в куче мест. - Дорогие поля оборачивайте в React.memo, а вычисления — в useMemo. ✍️ @React_lib

