ПРАКТИЧЕСКИЙ РАЗБОР · 7 минут

Как сделать форму с понятными ошибками в React

Форма заметки на React Hook Form и Zod с проверкой данных, сообщениями об ошибках и примером для Next.js

У формы создания заметки обычно простое начало. Поле заголовка, текст и кнопка «Сохранить».

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

Связка React Hook Form и Zod помогает собрать это аккуратнее. React Hook Form управляет полями и отправкой. Zod задаёт правила допустимых данных.

Сделаем небольшой пример на TypeScript.

Начинаем с правил, а не с кнопки

Допустим, пользователь создаёт заметку. Нам нужны два поля.

  • Заголовок от 3 до 80 символов.

  • Необязательный текст заметки длиной до 500 символов.

Правила можно описать одной схемой.

import { z } from "zod";

const noteSchema = z.object({
  title: z.string()
    .trim()
    .min(3, "Минимум 3 символа")
    .max(80, "Максимум 80 символов"),

  description: z.string()
    .max(500, "Максимум 500 символов"),
});

type NoteFormValues = z.infer<typeof noteSchema>;

trim() убирает пробелы по краям заголовка при проверке данных. Поэтому строка из одних пробелов не пройдёт минимальную длину.

Поле description в этом примере может оставаться пустым. Схема всё равно ожидает строку, просто не требует вводить текст.

TypeScript получает тип NoteFormValues из схемы. Не нужно отдельно описывать такой же объект и следить, чтобы две версии правил совпадали.

Подключаем React Hook Form

Установим три пакета.

npm install react-hook-form zod @hookform/resolvers

Теперь объединим форму и схему через zodResolver.

"use client";

import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const noteSchema = z.object({
  title: z.string()
    .trim()
    .min(3, "Минимум 3 символа")
    .max(80, "Максимум 80 символов"),
  description: z.string()
    .max(500, "Максимум 500 символов"),
});

type NoteFormValues = z.infer<typeof noteSchema>;

export default function NoteForm() {
  const [savedTitle, setSavedTitle] = useState("");

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<NoteFormValues>({
    resolver: zodResolver(noteSchema),
    defaultValues: {
      title: "",
      description: "",
    },
  });

  function onSubmit(values: NoteFormValues) {
    setSavedTitle(values.title);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="title">Заголовок</label>
      <input id="title" {...register("title")} />

      {errors.title && (
        <p role="alert">{errors.title.message}</p>
      )}

      <label htmlFor="description">Текст заметки</label>
      <textarea
        id="description"
        {...register("description")}
      />

      {errors.description && (
        <p role="alert">{errors.description.message}</p>
      )}

      <button type="submit">Проверить заметку</button>

      {savedTitle && (
        <p>Проверка пройдена: {savedTitle}</p>
      )}
    </form>
  );
}

Это работающая клиентская форма. После успешной проверки она показывает заголовок заметки. В базу данных пример ничего не записывает. Сохранение на сервер добавим следующим шагом.

Если заголовок пустой или слишком короткий, handleSubmit не вызовет onSubmit. Вместо этого пользователь увидит сообщение под нужным полем.

Здесь нет отдельного useState для каждого значения или ошибки. Регистрацией полей и результатами проверки занимается React Hook Form.

Почему ошибки лучше показывать возле поля

Представьте, что пользователь нажал «Сохранить» и получил сообщение «Неверные данные».

Ему всё ещё непонятно, что исправлять.

Сообщение «Минимум 3 символа» прямо под заголовком решает проблему быстрее. Для этого React Hook Form предоставляет formState.errors, а Zod возвращает причину отклонения значения.

Для длинной формы такая мелочь особенно полезна. Пользователь видит ошибку там, где она возникла.

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

Почему проверка на клиенте не заменяет серверную

React Hook Form помогает пользователю. Но браузер нельзя считать надёжной границей проверки данных.

В Next.js можно повторно проверить полученный объект на сервере той же Zod-схемой.

const result = noteSchema.safeParse(input);

if (!result.success) {
  return {
    ok: false,
    error: "Проверьте данные заметки",
  };
}

// Здесь можно сохранять result.data.

safeParse возвращает результат проверки, а не выбрасывает исключение из-за неверных полей.

Важная деталь состоит в том, что на сервере нужно проверять именно полученные данные. Нельзя полагаться на то, что браузер уже всё проверил.

В реальном приложении сервер также проверит, имеет ли пользователь право создать или изменить заметку.

Подробнее согласованные правила для клиента и сервера разобраны в моей статье на Habr.

Когда React Hook Form действительно нужен

Для формы из одного поля обычного React часто достаточно.

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

Zod при этом решает свою задачу. Он определяет, какие данные допустимы.

Их не обязательно использовать вместе в каждом проекте. Но для форм, которые со временем усложняются, такая связка хорошо снимает часть ручной работы.

Как форма становится частью приложения

На практике создание заметки не заканчивается проверкой заголовка.

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

Эти задачи мы разбираем в курсовом проекте Workbench Notes. Там формы связаны с созданием заметок, Server Actions, обновлениями интерфейса и автосохранением.

Разработчикам, знакомым с React и осваивающим надёжную разработку на TypeScript и Next.js, адресован курс «Next.js II: TypeScript 2026».

Начинать удобно именно с формы. На ней хорошо видно, как TypeScript, проверка данных и интерфейс начинают работать вместе.