Ошибки полей, ожидание и результат формы
Связываем серверное действие с полями, сообщениями и состоянием ожидания.
На этой странице
Для Next.js 16.3.8 (App Router) и React 19.3.0, Zod 4.6.5. Обновлено .
Теперь свяжем checkRegistration с интерфейсом. Пользователь должен видеть, какое поле исправить, понимать, что действие выполняется, и получать честное сообщение о результате.
Ошибки рядом с полями
Попробуйте имя из одного символа или неверный email.
Ввод сохраняется после проверки. Ошибки относятся к последней отправке. Проверка email здесь использует правила браузера; серверный пример ниже использует Zod.
Демонстрация проверяет только два поля в браузере и ничего не отправляет. Она показывает поведение интерфейса. Серверный вариант ниже использует все четыре поля и схему Zod из предыдущей статьи.
useActionState: ответ действия в компоненте
React предоставляет useActionState. Он возвращает:
state– последний ответ действия.formAction– функцию для<form action={...}>.pending– выполняется ли действие сейчас.
Здесь action получает функцию, а не строку адреса. React и Next.js организуют её вызов на сервере; самостоятельно писать fetch для этого действия не нужно. В этом примере передавайте formAction в атрибут action формы. Для вызова из обычного обработчика кнопки потребуется другой способ, описанный в документации.
Полный клиентский компонент
Замените созданную раньше страницу следующей парой файлов. actions.ts и lib/registration.ts должны остаться из предыдущей статьи.
"use client";
import { useActionState, useState } from "react";
import {
initialRegistrationState,
type RegistrationField,
} from "@/lib/registration";
import { checkRegistration } from "./actions";
const fields: {
name: RegistrationField;
label: string;
type: "text" | "email" | "date";
required: boolean;
autoComplete?: string;
}[] = [
{
name: "name",
label: "Имя",
type: "text",
required: true,
autoComplete: "name",
},
{
name: "email",
label: "Email",
type: "email",
required: true,
autoComplete: "email",
},
{ name: "date", label: "Дата мероприятия", type: "date", required: true },
{ name: "comment", label: "Комментарий", type: "text", required: false },
];
export function RegistrationForm() {
const [state, formAction, pending] = useActionState(
checkRegistration,
initialRegistrationState,
);
const [values, setValues] = useState({
name: "",
email: "",
date: "",
comment: "",
});
return (
<form action={formAction} className="grid max-w-lg gap-4">
{fields.map((field) => {
const errors = state.fieldErrors[field.name];
const inputId = `registration-${field.name}`;
const errorId = `${inputId}-error`;
return (
<div key={field.name} className="grid gap-1">
<label htmlFor={inputId}>{field.label}</label>
<input
id={inputId}
name={field.name}
type={field.type}
required={field.required}
autoComplete={field.autoComplete}
value={values[field.name]}
readOnly={pending}
onChange={(event) =>
setValues((current) => ({
...current,
[field.name]: event.target.value,
}))
}
aria-invalid={Boolean(errors?.length)}
aria-describedby={errors?.length ? errorId : undefined}
className="border-border bg-background text-foreground rounded-md border p-2"
/>
{errors?.length ? (
<p id={errorId} className="text-destructive">
{errors.join(" ")}
</p>
) : null}
</div>
);
})}
<button
type="submit"
disabled={pending}
className="bg-primary text-primary-foreground rounded-md p-2 disabled:opacity-50"
>
{pending ? "Проверяем…" : "Проверить заявку"}
</button>
<p role="status">{state.message}</p>
</form>
);
}Массив fields содержит настройки четырех полей. map создает одинаковую структуру подписи, поля и ошибки; key берется из стабильного имени. values хранит текущий ввод, state – ответ сервера на последнюю отправку. Это разные данные.
Управляемые поля сохраняют введенные значения после ответа действия. React может сбрасывать неуправляемые поля после успешно завершившегося действия, даже когда оно вернуло сообщения о неправильном вводе. Поэтому в нашем примере значения хранятся в состоянии и не зависят от автоматического сброса.
readOnly временно блокирует редактирование, но поле остается в FormData. disabled на самом поле исключил бы его из отправки. Кнопка отдельно отключена через disabled.
text-destructive появится после настройки токенов shadcn/ui. Если вы проходите пример без него, добавьте подходящую пару цветов ошибок для двух тем либо временно показывайте ошибки обычным цветом текста. Не оставляйте нечитаемый красный на красном.
import { RegistrationForm } from "./registration-form";
export default function RegisterPage() {
return (
<main>
<h1>Заявка на мероприятие</h1>
<p>Проверим поля. Эта учебная версия не сохраняет заявку.</p>
<RegistrationForm />
</main>
);
}В общей навигации из app/layout.tsx добавьте ссылку <Link href="/register">Заявка</Link>.
Ошибки должны быть доступны
aria-invalid отмечает неверное поле, aria-describedby связывает его с текстом ошибки. role="status" объявляет обновленное сообщение пользователю программы чтения с экрана. Цвет помогает заметить ошибку, а текст объясняет, как её исправить.
Сообщения относятся к последней отправке. В этом простом варианте они остаются до следующей проверки. Позже можно скрывать ошибку измененного поля или проверять его после ухода фокуса. Не показывайте «всё исправлено» только потому, что пользователь начал печатать.
Как показать именно серверную ошибку
Если обязательное поле пустое или email имеет неверный формат, браузер остановит отправку ещё до обращения к серверу. Это ожидаемое поведение.
Чтобы проверить схему, отправьте имя из одного символа и корректно заполните остальные обязательные поля. HTML пропустит такое имя, а Zod вернет ошибку. Для учебной проверки всех ответов сервера можно временно добавить noValidate к форме; потом уберите его. Проверка на сервере при этом не отключается.
pending может быть почти незаметен: действие выполняется быстро. Посмотреть состояние ожидания можно с замедлением сети в DevTools. Не добавляйте задержку в настоящую отправку только ради длинного индикатора.
Те же поля с shadcn/ui
Если уже добавлены button, input и label, импортируйте:
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";Замените input на Input, label на Label, button на Button и уберите их учебные классы оформления. Сохраните name, value, onChange, связь с ошибками и type="submit". Меняется оформление, а схема и серверное действие остаются прежними.
Попробуйте: неверное имя, неверная дата, длинный комментарий, корректные значения, Enter, повторная отправка, Tab и обе темы. После успешной проверки сообщение должно говорить о проверке, а не о несуществующем бронировании.
Документация: useActionState, форма React, формы Next.js, Input shadcn/ui.