AG & Dev

Валидация формы на стороне сервера при помощи React-хука useActionState

1 марта 2026 г.

Другие статьи

Dockerfile для Keystone CMS 6
Dockerfile и compose.yaml для развёртывания Keystone CMS 6
Аутентификация в Keystatic CMS
Настройка аутентификации в Keystatic CMS (Local mode) в проекте Next.js
Глобальное состояние в React
Глобальное состояние в React без подключения сторонних библиотек
Яндекс Карты в проекте Next.js
Подключение Яндекс Карты (API 3) к проекту Next.js (App Router)
Подключение к Headless CMS из Next.js
Пример подключения к Headless CMS (Wordpress) из приложения Next.js посредством REST API
Устранение FOUC в Next.js
Устранение FOUC-эффекта при использовании тёмной темы сайта в Next.js
Подключение Payload CMS к проекту Next.js
Как подключить Payload CMS к существующему проекту Next.js
Создание своего Rehype-плагина
Создание Rehype-плагина для корректировки HTML-кода при конвертировании из Markdown
Яндекс Метрика в проекте SvelteKit
Подключение счётчика Яндекс Метрики к проекту на SvelteKit (Svelte 5)
Анимация псевдоэлемента при помощи Anime.js
Анимация псевдоэлемента при помощи JavaScript-библиотеки Anime.js
Деплой сайта на хостинг через SFTP
Развёртывание сайта на хостинге в один клик
Липкий элемент с возможностью скроллинга
Скроллинг «липкого» элемента, превышающего высоту экрана, с помощью Svelte
Подключение FormBold с помощью Alpine.js
Подключение сервиса обработки форм FormBold с помощью фреймворка Alpine.js
Яндекс Метрика в проекте Next.js
Подключение счётчика Яндекс Метрики к проекту Next.js (App Router)
Все статьи

Технологический стек

AG & Dev

Разработка быстрых и надёжных веб-сайтов на базе самых современных технологий

Профиль на GitHub

Инструменты

React
Next.js
Tailwind

Платформы

Node.js
Docker
Nginx

CMS

Directus
Keystone
Keystatic

В React, начиная с версии 19, появился очень мощный хук useActionState, который позволяет максимально гибко и, при этом, без особых усилий настроить валидацию формы на стороне сервера.

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

Простой пример валидации на стороне сервера

1. Создаём компонент, использующий React-хук useActionState с валидацией формы на сервере и отображением ошибок валидации на клиенте:

src/ui/form/form.js
"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, которая будет выполняться на стороне сервера и осуществлять валидацию данных, отправляемых из формы:

src/ui/form/handler.js
"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
};

Рабочую версию этого решения можно проверить здесь.