Технологии

React Hook Form: зачем ИИ добавляет эту библиотеку в форму

24 минАктуально на 16 августа 2026

React Hook Form: формы с валидацией

Ты просишь Claude Code собрать форму обратной связи — три поля, кнопка «Отправить». В ответ появляется код, где вместо привычного <form> с парой <input> внутри стоят незнакомые вызовы: useForm(), register("email"), handleSubmit(onSubmit). Ни одного из этих слов не было в твоей просьбе, но без них форма почему-то не работает. Это React Hook Form — библиотека, которую ИИ подключает почти к каждой форме сложнее одного поля. Разберём, зачем она нужна и что означает каждая из этих загадочных строк.

Формы на курсе появляются раньше, чем кажется: заявка на консультацию на лендинге, форма заказа в интернет-магазине, редактирование профиля в личном кабинете. Как только полей становится больше одного, а хотя бы одно из них нужно проверять на корректность, у Claude Code почти всегда находится повод подключить именно React Hook Form — и полезно понимать, что именно он в этот момент делает с твоим кодом, а не просто доверять, что «так надо».

Содержание
  1. Что делает register() в коде, который написал ИИ
  2. Почему нельзя было обойтись обычным
  3. Что делает React Hook Form простыми словами
  4. Валидация через zod: ещё одна частая пара в сгенерированном коде
  5. При чём тут shadcn/ui и компонент Controller
  6. Обычный useState-подход и React Hook Form — что меняется
  7. Когда это оправдано, а когда хватит обычного useState
  8. Как проверить, что форма собрана правильно
  9. Частые вопросы
  10. Заключение

Что делает register() в коде, который написал ИИ

Открой файл с формой, которую собрал Claude Code. Скорее всего, он выглядит примерно так:

const { register, handleSubmit, formState: { errors } } = useForm();

function onSubmit(data) {
  console.log(data);
}

return (
  <form onSubmit={handleSubmit(onSubmit)}>
    <input {...register("email", { required: "Введите email" })} />
    {errors.email && <p>{errors.email.message}</p>}
    <button type="submit">Отправить</button>
  </form>
);

Ничего из этого не сломано и не лишнее. useForm() — точка входа: вызов, который создаёт для формы отдельный внутренний «блокнот», где React Hook Form будет хранить всё, что происходит с полями — значения, ошибки, факт того, что поле уже трогали. Из этого блокнота ты забираешь три инструмента, которые понадобятся дальше: register, handleSubmit и formState.

register("email", ...) — это не создание поля, а подключение уже существующего <input> к блокноту формы. Строчка {...register("email")} разворачивается в набор обычных HTML-атрибутов: name, onChange, onBlur, ref. Ты как будто говоришь библиотеке: «вот это поле зовут email, начни следить за ним». Второй аргумент, объект с required: "Введите email", — правило проверки: если поле останется пустым, ошибка появится с этим текстом.

handleSubmit(onSubmit) оборачивает твою функцию отправки. Когда пользователь жмёт кнопку, handleSubmit сначала сам проверяет все зарегистрированные поля по их правилам. Если хоть одно не прошло проверку — функция onSubmit вообще не вызывается, вместо этого заполняется объект errors. Если все поля в порядке — onSubmit получает готовый объект data со значениями всех полей формы, без единой строчки кода, которая бы их туда складывала вручную.

formState.errors — тот самый объект с ошибками, о котором уже шла речь. errors.email появляется, только если поле email не прошло проверку, и содержит текст ошибки, который ты задал в register. Строчка {errors.email && <p>{errors.email.message}</p>} — обычное условие React: «если ошибка есть, покажи параграф с текстом ошибки», знакомое из ранних уроков курса про «Змейку».

Собранная вместе, эта форма делает три вещи одновременно: отслеживает значения полей, проверяет их по правилам при отправке и показывает понятные ошибки прямо под нужным полем. Без единой строчки useState.

Обрати внимание и на саму запись {...register("email")} — три точки перед вызовом называются spread-синтаксис, обычная возможность JavaScript «распаковать» объект в отдельные атрибуты. register возвращает объект вроде { name: "email", onChange: ..., onBlur: ..., ref: ... }, а три точки превращают его в набор именно этих атрибутов прямо на элементе <input>, как если бы каждый был прописан отдельно. Ничего специфичного для React Hook Form в этом синтаксисе нет — та же запись пригодится и в других местах, где нужно быстро передать элементу целый набор готовых свойств разом.

Полезно знать ещё пару частых спутников register и handleSubmit, которые тоже попадаются в сгенерированном коде. formState.isSubmitting — булево значение, которое становится true на время отправки формы: удобно, чтобы заблокировать кнопку и не дать пользователю отправить форму дважды подряд. reset() очищает все поля до значений по умолчанию — обычно вызывается сразу после успешной отправки, чтобы форма не осталась заполненной старыми данными. defaultValues, который передают вторым аргументом в useForm({ defaultValues: {...} }), задаёт стартовые значения полей — пригождается в форме редактирования, где поля должны открыться уже заполненными текущими данными, а не пустыми.

Почему нельзя было обойтись обычным

Резонный вопрос: HTML и так умеет формы, зачем городить библиотеку поверх? Дело в том, что голая форма умеет собирать данные, но не умеет почти ничего из того, что нужно нормальному интерфейсу.

Обычный <form> с <input> внутри действительно отправит данные — но по умолчанию перезагрузит всю страницу, как в 2005 году, если этот стандартный переход специально не отменить. Проверки формата — «это похоже на email», «пароль не короче восьми символов» — HTML умеет ограниченно, через атрибуты вроде required и pattern с регулярным выражением, а вот показать ошибку красиво, ровно под нужным полем, и не отправлять форму дальше, если что-то не так, — это уже забота JavaScript.

В React голую форму пришлось бы собирать вручную, и вот что это значит на практике. На каждое поле — свой useState, который хранит текущее значение. На каждое поле — свой обработчик onChange, который обновляет это состояние при каждом нажатии клавиши. Отдельное состояние под ошибки, которое нужно проверять и очищать самому. Отдельная функция проверки формата email, которая вызывается в нужный момент и не забывает вернуть текст ошибки. Для формы из пяти полей это легко превращается в полсотни строк однотипного, повторяющегося кода, где легко забыть одну мелочь — например, очистить ошибку, когда пользователь наконец исправил поле.

Хуже другое: каждое нажатие клавиши в таком подходе — это обновление useState, а любое обновление состояния в React означает перерисовку компонента. Если форма большая, а поля завязаны на один общий объект состояния, перерисовывается вся форма целиком при каждой напечатанной букве в любом из полей. На простой форме это незаметно, а на форме побольше — с десятком полей, выпадающими списками, динамическими подсказками — начинает подтормаживать набор текста, и с этим тормозом придётся что-то делать отдельно.

Чтобы разница была не абстрактной, а видна глазами, вот форма из двух полей — email и пароль — написанная вручную, без всякой библиотеки:

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState({});

function handleSubmit(e) {
  e.preventDefault();
  const newErrors = {};
  if (!email) newErrors.email = "Введите email";
  if (password.length < 8) newErrors.password = "Минимум 8 символов";
  setErrors(newErrors);
  if (Object.keys(newErrors).length === 0) {
    console.log({ email, password });
  }
}

return (
  <form onSubmit={handleSubmit}>
    <input value={email} onChange={(e) => setEmail(e.target.value)} />
    {errors.email && <p>{errors.email}</p>}
    <input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
    {errors.password && <p>{errors.password}</p>}
    <button type="submit">Отправить</button>
  </form>
);

Пятнадцать строк ради двух полей, и это без учёта того, что ошибку из errors.email никто не очищает, пока пользователь заново не нажмёт «Отправить» — если человек уже начал печатать исправленный email, старое сообщение об ошибке так и провисит под полем до следующей попытки отправки. Та же форма через React Hook Form:

const { register, handleSubmit, formState: { errors } } = useForm();

return (
  <form onSubmit={handleSubmit((data) => console.log(data))}>
    <input {...register("email", { required: "Введите email" })} />
    {errors.email && <p>{errors.email.message}</p>}
    <input {...register("password", { minLength: { value: 8, message: "Минимум 8 символов" } })} type="password" />
    {errors.password && <p>{errors.password.message}</p>}
    <button type="submit">Отправить</button>
  </form>
);

Здесь на два поля не нужны ни useState, ни отдельная функция проверки, ни ручная сборка объекта с ошибками — и очистка старой ошибки при повторном вводе работает из коробки, без дополнительного кода. На форме из двух полей разница в объёме кода уже заметна, а с ростом числа полей она только увеличивается: у ручного подхода каждое новое поле добавляет пару строк состояния плюс строку проверки, у React Hook Form — одну строку register.

Что делает React Hook Form простыми словами

React Hook Form — библиотека, которая берёт на себя рутину управления формой: хранит значения полей, следит, что где менялось, проверяет их по заданным правилам и решает, когда форму можно отправлять, а когда нет. Вместо десятка useState и обработчиков на каждое поле ты один раз вызываешь useForm() и получаешь готовый набор инструментов для всей формы разом.

Ключевая техническая идея, ради которой библиотеку вообще стали использовать так широко, — она обходится без постоянных перерисовок компонента при вводе текста. Вместо привычного React-подхода «состояние меняется → компонент перерисовывается» библиотека подключается к полям напрямую через ref, почти как обычный JavaScript работал бы с DOM-элементами без React вообще, и обновляет значения в стороне от механизма перерисовки. Перерисовка происходит только тогда, когда это действительно нужно — например, чтобы показать сообщение об ошибке. Пользователь печатает в поле email — компонент формы не перерисовывается на каждую букву, потому что библиотеке для этого попросту не нужно трогать состояние React.

Для формы из трёх полей разница в скорости не заметна на глаз — там и обычный useState работает мгновенно. Прирост становится ощутимым, когда полей много или форма сложная: с выпадающими списками, полями, которые появляются в зависимости от выбора в других полях, живой проверкой пароля на надёжность по мере набора. Именно такие формы Claude Code чаще всего и собирает через React Hook Form — не потому что это модно, а потому что библиотека проектировалась ровно под этот случай.

За этим стоит различие, которое в документации React называют «управляемыми» и «неуправляемыми» полями. Управляемое поле — это когда значение <input> хранится в переменной состояния React и обновляется через onChange при каждом нажатии, то есть React в буквальном смысле управляет тем, что показано в поле, и должен перерисовывать компонент, чтобы это значение отобразить. Неуправляемое поле работает иначе: браузер сам хранит текущее значение внутри самого DOM-элемента, а React лишь изредка заглядывает туда через ref, когда значение действительно нужно — например, в момент отправки формы. React Hook Form по умолчанию использует именно второй способ, поэтому и не перерисовывает компонент на каждую букву: полю не нужно постоянно докладывать об изменениях наверх, оно просто хранит своё значение само, а библиотека забирает его, когда потребуется.

Валидация через zod: ещё одна частая пара в сгенерированном коде

Проверки в register("email", { required: "..." }) подходят для простых случаев — обязательное поле, минимальная длина. Когда правил становится больше — сложный формат телефона, пароль с определёнными требованиями, поле, которое обязательно только при определённом условии, — ИИ обычно не пишет это россыпью прямо в register, а подключает вторую библиотеку, zod, и описывает через неё единую схему проверки для всей формы сразу.

Схема zod выглядит как список правил на обычном JavaScript, без магии:

const schema = z.object({
  email: z.string().email("Некорректный email"),
  password: z.string().min(8, "Минимум 8 символов"),
});

Дальше эта схема одной строчкой подключается к useForm через специальный резолвер, и с этого момента React Hook Form сверяет данные формы именно с ней, а не с разрозненными правилами по каждому полю:

const { register, handleSubmit } = useForm({
  resolver: zodResolver(schema),
});

Разбирать здесь синтаксис zod подробно не будем — это тема отдельного урока, — но узнавать в коде эту связку полезно: увидел в проекте z.object({...}) рядом с useForm — значит, вся проверка формы описана в одном месте, а не размазана по атрибутам каждого поля. Есть и практическое удобство, которое ценят особенно в связке с TypeScript: схема zod одновременно описывает и правила проверки, и форму самих данных, поэтому не приходится отдельно писать тип для объекта формы и отдельно — правила валидации, рискуя, что они разъедутся между собой после очередной правки.

Zod — не единственная библиотека для описания схем такого рода, у неё есть аналоги вроде Yup. Но именно zod чаще всего встречается в коде, который пишут современные ИИ-инструменты для проектов на React и Next.js, поэтому в сгенерированном коде курса ты, скорее всего, увидишь именно её.

При чём тут shadcn/ui и компонент Controller

Если Claude Code уже собирал для проекта интерфейс через shadcn/ui, то компонент form.tsx из папки components/ui — не отдельная, третья библиотека форм, а обёртка именно над React Hook Form. Внутри у него те же useForm, register и handleSubmit, только аккуратно завёрнутые в компоненты FormField, FormItem и FormMessage, которые сами расставляют подпись поля, текст ошибки и нужные отступы по макету shadcn/ui. Работа с формой остаётся той же самой — просто верхний слой вёрстки уже готов, и не нужно вручную писать {errors.email && <p>...} под каждым полем.

Здесь же всплывает практическая тонкость, из-за которой в коде иногда встречается ещё один вызов — Controller. register отлично работает с обычными HTML-полями вроде <input>, потому что там есть простые события onChange и onBlur, к которым легко подключиться напрямую. А вот кастомный компонент — выпадающий список Select из shadcn/ui, переключатель Switch, календарь для выбора даты — устроен иначе: он не всегда работает через обычный <input> под капотом и не всегда одинаково сообщает о своём значении наружу. Для таких компонентов React Hook Form предлагает Controller — обёртку, которая берёт на себя связь между нестандартным компонентом и общим блокнотом формы:

<Controller
  control={control}
  name="country"
  render={({ field }) => (
    <Select onValueChange={field.onChange} value={field.value} />
  )}
/>

Видеть Controller в коде — не повод для тревоги, это тот же самый механизм подключения поля к форме, что и register, просто для компонента, который устроен не как обычный <input>.

Обычный useState-подход и React Hook Form — что меняется

useState-подход React Hook Form
Код на одно поле useState + обработчик onChange + ручная проверка одна строка register("поле", {правило})
Перерисовка при вводе компонент перерисовывается на каждое нажатие клавиши перерисовки при вводе почти нет
Валидация пишется вручную для каждого поля встроена через правила или схему zod
Ошибки под полем собираются и очищаются вручную готовый объект errors с текстом
Отправка формы ручная проверка перед вызовом API handleSubmit сам проверяет и блокирует отправку при ошибках
Объём кода на форму из 5+ полей растёт линейно, легко пропустить мелочь почти не растёт с числом полей
Очистка старой ошибки при вводе нужно писать отдельно работает по умолчанию
Кастомные компоненты (Select, Switch) подключаются как обычное состояние подключаются через Controller
Отправка формы во время загрузки нужно вручную следить за флагом готовый formState.isSubmitting

Из таблицы видно главное: разница не в том, что один подход «правильный», а другой «неправильный», а в том, сколько рутины ты перекладываешь на библиотеку. Чем больше в форме полей и правил проверки, тем заметнее выигрыш.

Когда это оправдано, а когда хватит обычного useState

React Hook Form не универсальный ответ на любую форму, и хороший ИИ это понимает, хотя иногда всё равно подключает библиотеку по привычке даже там, где она не нужна.

Библиотека действительно оправдана там, где несколько полей, каждое из которых нужно проверять, а ошибки должны появляться рядом с конкретным полем: форма регистрации, форма заказа, анкета с обязательными и необязательными полями вперемешку. Здесь ручной useState-подход быстро превращается в источник мелких багов — забытую проверку, ошибку, которая не очищается после исправления поля.

Для формы из одного поля — например, строка подписки на рассылку с единственным <input type="email"> и кнопкой — подключать React Hook Form избыточно. Здесь хватит одного useState под значение поля и простой проверки перед отправкой: библиотека, которая создавалась для управления десятком полей разом, для одного поля добавляет только лишнюю зависимость в проекте и лишнюю прослойку кода, не давая взамен ничего, что нельзя было бы написать в три строки самому.

Если Claude Code подключил React Hook Form к форме из одного поля, а тебе кажется, что это слишком сложно для такой мелочи — можно прямо попросить упростить: «замени React Hook Form на обычный useState, поле одно, библиотека тут ни к чему». Это разумная и понятная ИИ просьба, а не спор с его решением.

Есть и промежуточный случай, который на курсе встречается регулярно: форма из двух-трёх полей, где важна не сложная логика проверки, а именно то, что каждое поле умеет само сообщать о своей ошибке рядом с собой, без ручной синхронизации нескольких useState. Здесь выбор не такой однозначный, и решение стоит принимать по количеству правил, а не по количеству полей: форма с двумя полями и жёсткими требованиями к паролю выигрывает от React Hook Form не меньше, чем форма из пяти простых текстовых полей без всякой проверки формата.

Как проверить, что форма собрана правильно

Когда Claude Code выдаёт готовый код формы, полезно бегло свериться с несколькими вещами, прежде чем считать задачу закрытой.

Во-первых, у каждого поля, которое участвует в отправке, должен быть свой register("имяПоля", ...) — если поле просто стоит на странице без него, оно не попадёт в данные формы при отправке. Во-вторых, у <form> тег onSubmit должен ссылаться на handleSubmit(твояФункция), а не напрямую на твою функцию — если обёртки handleSubmit нет, проверки полей просто не сработают, и форма отправится, даже если обязательные поля пустые. В-третьих, под каждым проверяемым полем в разметке должен быть код вида {errors.имяПоля && ...} — без него ошибка технически посчитается, но пользователь её не увидит и не поймёт, почему форма не отправляется.

Проверить это на практике проще всего вживую: открой форму в браузере, оставь обязательное поле пустым и нажми «Отправить». Если под полем появляется текст ошибки, а сама форма не отправляется — всё подключено верно. Если форма всё равно отправляется с пустыми полями — стоит попросить Claude Code проверить, что handleSubmit действительно оборачивает функцию отправки, а не вызывается отдельно от неё.

Стоит проверить и обратный случай: заполнить форму полностью корректными данными и убедиться, что после нажатия «Отправить» действительно происходит то, что ожидалось — данные ушли на сервер, появилось сообщение об успехе, поля очистились через reset(). Формы с проверками иногда «зависают» в состоянии загрузки, если забыли выключить isSubmitting после ответа сервера, — тогда кнопка так и останется заблокированной, а пользователь решит, что сайт завис.

Ещё один частый сценарий на курсе — форма, где одно поле должно зависеть от значения другого: например, поле «телефон» становится обязательным, только если выбран способ доставки «курьер», а не «самовывоз». Такую логику вручную на useState пришлось бы городить через дополнительные условия при каждом рендере. В React Hook Form для этого есть функция watch("способДоставки"), которая возвращает текущее значение нужного поля прямо во время рендера компонента, и по этому значению можно решить, показывать ли поле «телефон» вообще и добавлять ли для него правило required. Встретив watch в сгенерированном коде, можно быть уверенным: где-то ниже одно поле влияет на поведение другого.

Частые вопросы

Нужно ли новичку учить React Hook Form отдельно, чтобы работать с ИИ?

Нет, для старта достаточно узнавать в коде три вещи: useForm создаёт форму, register подключает поле, handleSubmit отправляет данные после проверки. Когда нужно что-то поправить, проще напрямую попросить Claude Code: «в этой форме добавь проверку, что пароль не короче восьми символов» — ИИ сам знает синтаксис библиотеки и внесёт правку.

Что будет, если удалить register() у поля?

Поле перестанет быть частью формы: React Hook Form не будет знать о его значении, оно не попадёт в объект data при отправке и не будет проверяться по правилам. Само поле при этом останется на странице и будет принимать ввод как обычный HTML-элемент, просто форма его больше не видит.

Почему в одних формах ИИ пишет проверки прямо в register, а в других подключает zod?

Обычно это вопрос числа и сложности правил. Пара простых проверок вроде «обязательное поле» и «минимум 8 символов» умещаются прямо в register без потери читаемости. Когда правил становится много или они пересекаются между полями — например, «поле обязательно, только если выбран другой пункт», — все правила выносят в одну схему zod, чтобы не искать логику проверки по всему файлу формы.

Можно ли использовать React Hook Form без TypeScript?

Да, библиотека прекрасно работает в обычном JavaScript. TypeScript добавляет только дополнительную проверку типов на этапе написания кода — например, подскажет, если пытаешься зарегистрировать поле, которого нет в схеме. Для функциональности формы это не обязательно.

Форма отправляется, хотя поле явно пустое — в чём может быть проблема?

Скорее всего, у поля не задано правило required в register, либо оно зарегистрировано, но правило указано с опечаткой в названии. Стоит попросить Claude Code показать содержимое register для этого поля и добавить недостающее правило проверки.

Замедляет ли React Hook Form простую форму из двух-трёх полей?

Нет, ни в какую сторону это не критично: библиотека лёгкая, а её преимущество в скорости заметно на больших формах, а не мешает на маленьких. Вопрос для формы из пары полей не в скорости, а в оправданности лишней зависимости — иногда проще обойтись обычным useState, как описано выше.

Зачем в коде появляется Controller, если есть register?

register подключает к форме обычные HTML-поля вроде <input> или <textarea>. Controller нужен там, где поле — не простой HTML-элемент, а собранный компонент вроде выпадающего списка Select из shadcn/ui или переключателя даты, который не сообщает о своих изменениях так же напрямую, как обычный <input>. Оба варианта делают одно и то же — подключают поле к общему блокноту формы, просто разными способами в зависимости от типа поля.

Форма из shadcn/ui и форма на голом React Hook Form — это разные вещи?

Нет, это один и тот же механизм на разных уровнях. Компонент form.tsx из shadcn/ui — обёртка поверх React Hook Form, которая добавляет готовую вёрстку подписи поля и текста ошибки, но логика внутри та же самая: useForm, register, handleSubmit. Если видишь в проекте FormField вместо прямого register — это не другая библиотека, а её же, только с готовым оформлением сверху.

Что делать, если Claude Code добавил слишком много правил проверки для учебной формы?

Можно прямо попросить упростить: «убери проверку сложности пароля, оставь только минимальную длину» или «сделай поле телефона необязательным». React Hook Form и zod одинаково легко и добавляют, и убирают отдельные правила — это точечная правка одной строки в схеме или в аргументах register, а не переписывание формы целиком.

Заключение

register, handleSubmit и useForm в сгенерированном коде — не усложнение ради усложнения, а способ убрать однотипную рутину: ручной useState на каждое поле, ручные обработчики, ручную сборку ошибок под каждым полем. React Hook Form берёт эту работу на себя и не заставляет компонент перерисовываться на каждую напечатанную букву, что особенно заметно на формах с большим числом полей. Для сложной формы с проверками — оправданный выбор, который тебе не придётся объяснять ИИ отдельно, он это уже знает. Для одного поля подписки хватит и простого useState, и попросить об этом Claude Code — нормально. Стиль вёрстки внутри такой формы обычно всё тот же, что разобран в статьях про Tailwind CSS и shadcn/ui — React Hook Form отвечает за поведение формы, а не за то, как она выглядит. Если решишь собрать полноценный лендинг с формой заявки, в статье про сборку лендинга на Next.js с Claude показано, как эти части складываются в единую страницу.

Читай дальше

Все статьи

Не просто статьи — тебя доведут до результата

В практикуме за 2499 ₽ рядом живая команда практикующих разработчиков и маркетологов: ведём по шагам до твоего работающего приложения. Не «ролики и сам разбирайся» — помогаем на каждом затыке.

Перейти к практикуму
Все статьи Ещё: технологии и архитектура