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

1 марта 2026 г.

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

В этой статье я покажу простейший пример валидации и обработки формы на стороне сервера при помощи React-хука useActionState.

Решение

1. Создаём функцию serverAction, которая будет выполняться на стороне сервера и осуществлять валидацию и обработку данных, отправляемых из формы:

src/ui/form/actions.js
"use server";
 
import { z } from "zod";
 
const formSchema = z.object({
  firstname: z.string().min(1),
  lastname: z.string().min(1),
  email: z.email(),
  phone: z
    .string()
    .transform((val) => val.replace(/[^\d+]/g, ""))
    .refine((val) => /^\+?[1-9]\d{9,14}$/.test(val)),
});
 
export const serverAction = async (prevState, formData) => {
  const data = {
    firstname: formData.get("firstname"),
    lastname: formData.get("lastname"),
    email: formData.get("email"),
    phone: formData.get("phone"),
  };
 
  const validate = formSchema.safeParse(data);
 
  if (validate.success) {
    const result = await formHandler(data);
 
    if (result) {
      return {
        validate: {},
        success: true,
        error: false,
      };
    } else {
      return {
        validate: {},
        success: false,
        error: true,
      };
    }
  } else {
    return {
      validate: z.treeifyError(validate.error).properties,
      success: false,
      error: false,
    };
  }
};
 
const formHandler = async (data) => {
  // здесь должен быть обработчик формы
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return true; // или false
};

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

src/ui/form/form.js
"use client";
 
import clsx from "clsx";
 
import { useState, useEffect, useActionState } from "react";
import { serverAction } from "./actions";
 
const initFields = {
  firstname: "",
  lastname: "",
  email: "",
  phone: "",
};
 
const initState = {
  validate: {},
  success: false,
  error: false,
};
 
export default function Form() {
  const [state, formAction, isPending] = useActionState(
    serverAction,
    initState,
  );
 
  const [fields, setFields] = useState(initFields);
  const [validate, setValidate] = useState({});
 
  const changeFileld = (field, value) => {
    setFields((prev) => ({ ...prev, [field]: value }));
    setValidate((prev) => ({ ...prev, [field]: undefined }));
  };
 
  useEffect(() => {
    setValidate(state.validate);
 
    if (state.success) {
      setFields(initFields);
      console.log("Форма успешно отправлена");
    } else if (state.error) {
      console.log("Ошибка при отправке формы");
    }
  }, [state]);
 
  return (
    <form action={formAction} noValidate>
      <div>
        <input
          type="text"
          name="firstname"
          value={fields.firstname}
          onChange={(e) => changeFileld("firstname", e.target.value)}
          className={clsx({ "border-red-500": validate?.firstname })}
          placeholder="Имя"
        />
 
        <div
          className={clsx("text-red-500", { invisible: !validate?.firstname })}>
          Укажите своё имя
        </div>
      </div>
 
      <div>
        <input
          type="text"
          name="lastname"
          value={fields.lastname}
          onChange={(e) => changeFileld("lastname", e.target.value)}
          className={clsx({ "border-red-500": validate?.lastname })}
          placeholder="Фамилия"
        />
 
        <div
          className={clsx("text-red-500", { invisible: !validate?.lastname })}>
          Укажите свою фамилию
        </div>
      </div>
 
      <div>
        <input
          type="email"
          name="email"
          value={fields.email}
          onChange={(e) => changeFileld("email", e.target.value)}
          className={clsx({ "border-red-500": validate?.email })}
          placeholder="Email"
        />
 
        <div className={clsx("text-red-500", { invisible: !validate?.email })}>
          Введите корректный Email-адрес
        </div>
      </div>
 
      <div>
        <input
          type="tel"
          name="phone"
          value={fields.phone}
          onChange={(e) => changeFileld("phone", e.target.value)}
          className={clsx({ "border-red-500": validate?.phone })}
          placeholder="Телефон"
        />
 
        <div className={clsx("text-red-500", { invisible: !validate?.phone })}>
          Введите корректный номер телефона
        </div>
      </div>
 
      <div>
        {isPending ? (
          <button type="button">Отправляется</button>
        ) : (
          <button type="submit">Отправить</button>
        )}
      </div>
    </form>
  );
}

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