В React, начиная с версии 19, появился очень мощный хук useActionState, который позволяет максимально гибко и, при этом, без особых усилий настроить валидацию формы на стороне сервера.
Валидация формы на стороне сервера обычно используется при регистрации, авторизации и других операциях с повышенными требованиями к безопасности. Кроме того, валидация формы на стороне сервера позволяет реализовать дополнительный функционал, выполняемый на сервере и невидимый на клиенте, например, свою уникальную защиту от спама, не зависящую от внешних сервисов.
1. Создаём компонент, использующий React-хук useActionState с валидацией формы на сервере и отображением ошибок валидации на клиенте:
"use client";
import { clsx } from "clsx";
import { useState, useActionState, useEffect } from "react";
import { formHandler } from "./handler";
const initStatus = {
success: "",
error: "",
};
const initData = {
subject: "",
message: "",
email: "",
};
const initChange = {
subject: false,
message: false,
email: false,
};
const initState = {
status: initStatus,
values: initData,
errors: initData,
};
export default function Feedback() {
const [changeState, setChangeState] = useState(initChange);
const [formState, formAction, isPending] = useActionState(
formHandler,
initState,
);
useEffect(() => {
(() => setChangeState(initChange))();
if (formState.status.success) {
// всплывающее сообщение об успешной отправке
console.log(formState.status.success);
} else if (formState.status.error) {
// всплывающее сообщение об ошибке отправки
console.log(formState.status.error);
}
}, [formState]);
return (
<form action={formAction} noValidate>
<div>
<input
type="text"
name="subject"
defaultValue={formState.values.subject}
onChange={() =>
setChangeState((prev) => ({ ...prev, subject: true }))
}
disabled={isPending}
className={clsx({
"border-red-500": formState.errors.subject && !changeState.subject,
})}
placeholder="Тема"
/>
{formState.errors.subject && !changeState.subject && (
<div className="text-red-500">{formState.errors.subject}</div>
)}
</div>
<div>
<textarea
name="message"
defaultValue={formState.values.message}
onChange={() =>
setChangeState((prev) => ({ ...prev, message: true }))
}
disabled={isPending}
className={clsx({
"border-red-500": formState.errors.message && !changeState.message,
})}
placeholder="Сообщение"></textarea>
{formState.errors.message && !changeState.message && (
<div className="text-red-500">{formState.errors.message}</div>
)}
</div>
<div>
<input
type="email"
name="email"
defaultValue={formState.values.email}
onChange={() => setChangeState((prev) => ({ ...prev, email: true }))}
disabled={isPending}
className={clsx({
"border-red-500": formState.errors.email && !changeState.email,
})}
placeholder="Email"
/>
{formState.errors.email && !changeState.email && (
<div className="text-red-500">{formState.errors.email}</div>
)}
</div>
<div>
<button type="submit" disabled={isPending}>
Отправить
</button>
</div>
</form>
);
}Пример является нестилизованным, за исключением пары стилей Tailwind, необходимых для выделения ошибок валидации красной рамкой у поля и красной подсказкой под полем.
2. Создаём функцию formHandler, которая будет выполняться на стороне сервера и осуществлять валидацию данных, отправляемых из формы:
"use server";
import { z } from "zod";
const formSchema = z.object({
subject: z.string().min(1),
message: z.string().min(1),
email: z.email(),
});
const initStatus = {
success: "",
error: "",
};
const initData = {
subject: "",
message: "",
email: "",
};
export const formHandler = async (prevState, formData) => {
const data = {
subject: formData.get("subject"),
message: formData.get("message"),
email: formData.get("email"),
};
const validate = formSchema.safeParse(data);
if (!validate.success) {
const errors = z.treeifyError(validate.error).properties;
return {
status: initStatus,
values: data,
errors: {
subject: errors.subject ? "Укажите тему сообщения" : "",
message: errors.message ? "Сообщение не может быть пустым" : "",
email: errors.email ? "Укажите корректный Email-адрес" : "",
},
};
}
const success = await sendMessage(data);
if (success) {
return {
status: {
success: "Сообщение успешно отправлено",
error: "",
},
values: initData,
errors: initData,
};
} else {
return {
status: {
success: "",
error: "Ошибка отправки сообщения",
},
values: data,
errors: initData,
};
}
};
const sendMessage = async (data) => {
// здесь код отправки сообщения
return true; // или false
};Рабочую версию этого решения можно проверить здесь.