Next-03. Формы и проверка данных
Проходим путь формы от HTML до серверной проверки в Next.js.
На этой странице
Одна форма – несколько уровней
Форма начинается с HTML: браузер показывает поля, собирает введённые значения и умеет проверять простые ограничения. JavaScript может выполнить код при отправке. React описывает части интерфейса компонентами, а Next.js позволяет передать отправку серверной функции. Если слова «React» и «компонент» пока незнакомы, сначала прочитайте введение в Next.js.
Полезно разделять три задачи:
- HTML описывает поля, подписи и кнопку отправки.
- Проверка данных отвечает, допустимы ли введённые значения.
- Серверное действие решает, что делать с верными данными: сохранить их, отправить письмо или вернуть результат.
От HTML к Next.js
Минимальная нативная форма:
<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 может отменить стандартную отправку и прочитать этот набор:
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.
import * as z from "zod";
export const signupSchema = z.object({
email: z.email("Введите корректный адрес"),
});z.object описывает ожидаемую форму данных, а z.email проверяет адрес. В следующем примере серверная функция читает адрес из FormData и передаёт его в safeParse. Этот метод возвращает понятный результат проверки: при успехе используем проверенные данные из result.data, при ошибке останавливаем обработку.
"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 для подключения возможностей вроде состояния к компоненту. Здесь он хранит ответ сервера и сообщает, выполняется ли отправка:
"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 меняют внешний вид полей и кнопки, а отправка и проверка остаются прежними. Сначала добавьте импорты вверху файла:
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";Затем замените содержимое return в SignupForm этим фрагментом:
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.
Задание
Сделайте форму записи на консультацию:
- Поля: имя, электронная почта, дата и комментарий.
- Добавьте подписи и подходящие HTML-типы полей.
- Создайте схему Zod: имя не короче двух символов, корректная почта и обязательная дата. Поле
input type="date"передаёт дату строкой; проверьте её форматYYYY-MM-DDчерезz.iso.date(). - Покажите ошибки рядом с полями и сообщение об успешной отправке.
- Проверьте форму и на сервере. Не считайте браузерную проверку защитой данных.
Для знакомства с асинхронным кодом посмотрите видео про событийный цикл JavaScript. Это отдельная тема языка: форма лишь показывает место, где асинхронная работа часто встречается.