Skip to content

Next-03. Формы и проверка данных

Проходим путь формы от HTML до серверной проверки в Next.js.

On this page

Одна форма – несколько уровней

Форма начинается с HTML: браузер показывает поля, собирает введённые значения и умеет проверять простые ограничения. JavaScript может выполнить код при отправке. React описывает части интерфейса компонентами, а Next.js позволяет передать отправку серверной функции. Если слова «React» и «компонент» пока незнакомы, сначала прочитайте введение в Next.js.

Полезно разделять три задачи:

  1. HTML описывает поля, подписи и кнопку отправки.
  2. Проверка данных отвечает, допустимы ли введённые значения.
  3. Серверное действие решает, что делать с верными данными: сохранить их, отправить письмо или вернуть результат.

От HTML к Next.js

Минимальная нативная форма:

index.html
<form id="signup-form" action="/signup" method="post">
  <label for="email">Электронная почта</label>
  <input id="email" name="email" type="email" required />
  <button type="submit">Зарегистрироваться</button>
</form>

name даёт полю ключ в отправляемых данных, а required и type="email" включают встроенные проверки браузера. Это полезная обратная связь, но сервер всё равно обязан проверить пришедшие данные: браузерные ограничения можно обойти.

/signup в примере – адрес обработчика формы. Чтобы форма завершилась успешно, приложение должно принять запрос по этому адресу. Если такого обработчика нет, сервер вернёт ошибку 404.

Атрибут name становится именем поля в наборе отправляемых значений. JavaScript может отменить стандартную отправку и прочитать этот набор:

main.js
const form = document.querySelector("#signup-form");
 
form.addEventListener("submit", (event) => {
  event.preventDefault();
  const formData = new FormData(form);
  console.log(formData.get("email"));
});

document.querySelector("#signup-form") ищет форму по её id. addEventListener("submit", ...) просит браузер вызвать переданную функцию при отправке; он передаёт ей event – сведения о событии. FormData собирает пары «имя поля – введённое значение», а get("email") читает значение поля email. event.preventDefault() отменяет обычную отправку и перезагрузку страницы. Здесь функция только печатает адрес в консоль; для сохранения данных нужен обработчик на сервере.

В обычном JavaScript обработчик submit может прочитать FormData и показать результат без перезагрузки. В React обработчик события задают компоненту, а в Next.js форму можно связать с серверной функцией, которая получит FormData. Официальное руководство показывает этот путь с серверными функциями и формами.

Что означают директивы

"use client" в начале файла задает границу клиентского компонента: он нужен для состояния, событий вроде onSubmit и API браузера. Остальные компоненты App Router могут оставаться серверными. Не добавляйте директиву автоматически во все файлы.

В примере директива "use server" стоит в начале файла actions.ts: она помечает его экспортируемую функцию signup как серверную. Её код выполняется на сервере, а через форму она получает отправленные поля. Проверять права пользователя и входные данные нужно внутри такой функции. Слово async означает, что функция может ожидать результат операции; это разобрано в статье об асинхронном коде. Подробности о директивах: use client и use server.

Проверка на сервере с Zod

Данные из браузера нельзя считать правильными только потому, что поле было проверено на странице: запрос можно отправить и другим способом. TypeScript помогает находить ошибки в коде до запуска, но сам не проверяет введённое значение. Схема Zod проверяет его во время работы программы:

Перед примером установите Zod в проект, например командой bun add zod или npm install zod. Примеры ниже используют актуальный синтаксис Zod 4.

app/signup-schema.ts
import * as z from "zod";
 
export const signupSchema = z.object({
  email: z.email("Введите корректный адрес"),
});

z.object описывает ожидаемую форму данных, а z.email проверяет адрес. В следующем примере серверная функция читает адрес из FormData и передаёт его в safeParse. Этот метод возвращает понятный результат проверки: при успехе используем проверенные данные из result.data, при ошибке останавливаем обработку.

app/actions.ts
"use server";
 
import { signupSchema } from "./signup-schema";
 
export async function signup(
  _previousState: { message: string },
  formData: FormData,
) {
  const result = signupSchema.safeParse({
    email: formData.get("email"),
  });
 
  if (!result.success) {
    return { message: "Проверьте адрес электронной почты" };
  }
 
  return { message: `Формат адреса ${result.data.email} принят` };
}

Чтобы соединить серверную функцию с отправкой формы и показать результат, используем React useActionState. Хук – это функция React для подключения возможностей вроде состояния к компоненту. Здесь он хранит ответ сервера и сообщает, выполняется ли отправка:

app/signup-form.tsx
"use client";
 
import { useActionState } from "react";
import { signup } from "./actions";
 
const initialState = { message: "" };
 
export function SignupForm() {
  const [state, formAction, pending] = useActionState(signup, initialState);
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Электронная почта</label>
      <input id="email" name="email" type="email" required />
      <p aria-live="polite">{state.message}</p>
      <button disabled={pending} type="submit">
        {pending ? "Проверяем..." : "Отправить"}
      </button>
    </form>
  );
}

useActionState получает серверную функцию и начальный ответ. Он возвращает текущий ответ state, функцию отправки formAction и признак выполнения pending. Атрибут aria-live="polite" сообщает программам чтения с экрана, что текст ответа обновился.

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

Та же форма с shadcn/ui

Компоненты shadcn/ui меняют внешний вид полей и кнопки, а отправка и проверка остаются прежними. Сначала добавьте импорты вверху файла:

app/signup-form.tsx
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

Затем замените содержимое return в SignupForm этим фрагментом:

app/signup-form.tsx
return (
  <form action={formAction}>
    <div className="grid gap-2">
      <Label htmlFor="email">Электронная почта</Label>
      <Input id="email" name="email" type="email" required />
      <p aria-live="polite">{state.message}</p>
    </div>
 
    <Button disabled={pending} type="submit">
      {pending ? "Проверяем..." : "Отправить"}
    </Button>
  </form>
);

Input, Label и Button - компоненты проекта из shadcn/ui. Подпись по-прежнему связана с полем через htmlFor и id, name передает значение в FormData, а Zod проверяет его в серверной функции. UI-компонент не заменяет ни HTML-структуру, ни проверку данных. Актуальные примеры: компонент Input, Label и Button.

Задание

Сделайте форму записи на консультацию:

  1. Поля: имя, электронная почта, дата и комментарий.
  2. Добавьте подписи и подходящие HTML-типы полей.
  3. Создайте схему Zod: имя не короче двух символов, корректная почта и обязательная дата. Поле input type="date" передаёт дату строкой; проверьте её формат YYYY-MM-DD через z.iso.date().
  4. Покажите ошибки рядом с полями и сообщение об успешной отправке.
  5. Проверьте форму и на сервере. Не считайте браузерную проверку защитой данных.

Для знакомства с асинхронным кодом посмотрите видео про событийный цикл JavaScript. Это отдельная тема языка: форма лишь показывает место, где асинхронная работа часто встречается.