1. Введение
Давайте честно: отправка формы — это только полдела. Пользователь хочет видеть, что происходит: всё ли получилось, что не так, если возникла ошибка, или наоборот — поздравление с успехом. И конечно, хочется сразу подсвечивать ошибки валидации, чтобы не пришлось гадать, почему сервер молчит.
В "старых добрых" React-приложениях мы писали кучу кода для управления состоянием формы: хранили значения полей, ошибки, статусы загрузки, отправляли данные через fetch/AJAX и т.д.
В мире Server Actions всё стало проще, но управление состоянием всё равно нужно. Вот тут и появляется useFormState — хук, который позволяет:
- Хранить и обновлять состояние формы (например, ошибки, сообщения, результат отправки).
- Реагировать на результат Server Action прямо в клиентском компоненте.
- Делать формы интерактивными без сложных костылей.
Как работает useFormState?
useFormState — это React-хук, который связывает состояние формы с серверным action. Он принимает два аргумента:
- Action — серверная функция (Server Action), которая будет вызываться при отправке формы.
- InitialState — начальное состояние формы (например, { message: '' }, { errors: {} } и т.д.).
Хук возвращает массив из двух элементов:
- state — текущее состояние формы (например, ошибки, сообщения).
- formAction — функция, которую нужно передать в атрибут action формы.
Вот базовый синтаксис:
const [state, formAction] = useFormState(serverAction, initialState);
state обновляется автоматически после каждого отправления формы — на основании того, что возвращает ваша Server Action.
2. Простой пример: форма обратной связи
Давайте создадим простую форму отправки сообщения с обработкой ошибок через useFormState.
Серверный action
// app/actions/sendMessage.js
"use server";
export async function sendMessage(prevState, formData) {
const message = formData.get("message");
if (!message || message.length < 5) {
return { error: "Сообщение должно быть не короче 5 символов" };
}
// Здесь могла бы быть отправка в базу данных...
return { success: "Спасибо за сообщение!" };
}
Клиентский компонент с useFormState
// app/components/ContactForm.jsx
"use client";
import { useFormState } from "react-dom";
import { sendMessage } from "../actions/sendMessage";
const initialState = {};
export default function ContactForm() {
const [state, formAction] = useFormState(sendMessage, initialState);
return (
<form action={formAction}>
<textarea name="message" placeholder="Ваше сообщение" />
<button type="submit">Отправить</button>
{/* Вывод сообщений об ошибке или успехе */}
{state.error && <div style={{ color: "red" }}>{state.error}</div>}
{state.success && <div style={{ color: "green" }}>{state.success}</div>}
</form>
);
}
Что здесь происходит?
- При отправке формы вызывается серверная функция sendMessage.
- Функция возвращает объект с ошибкой или успехом.
- useFormState автоматически обновляет state, и мы можем отобразить пользователю нужное сообщение.
Как работает передача состояния
Вся магия useFormState строится на том, что серверная функция (Server Action) всегда принимает два аргумента:
- prevState — предыдущее состояние формы (например, ошибки, значения).
- formData — данные, пришедшие из формы.
Server Action возвращает новое состояние, которое автоматически попадает в state на клиенте.
Важно: useFormState работает только с формами, которые отправляются через Server Actions (action={formAction}), а не через обычные fetch-запросы.
3. Валидация и обработка ошибок
Один из главных плюсов useFormState — простота обработки ошибок. Ваша Server Action может возвращать любые данные: ошибки, поля, сообщения и т.д. На клиенте вы просто отображаете их.
Пример: форма регистрации с валидацией
// app/actions/register.js
"use server";
export async function register(prevState, formData) {
const email = formData.get("email");
const password = formData.get("password");
const errors = {};
if (!email || !email.includes("@")) {
errors.email = "Некорректный email";
}
if (!password || password.length < 6) {
errors.password = "Пароль слишком короткий";
}
if (Object.keys(errors).length > 0) {
return { errors };
}
// Здесь могла бы быть регистрация пользователя...
return { success: "Регистрация успешна!" };
}
// app/components/RegisterForm.jsx
"use client";
import { useFormState } from "react-dom";
import { register } from "../actions/register";
const initialState = {};
export default function RegisterForm() {
const [state, formAction] = useFormState(register, initialState);
return (
<form action={formAction}>
<div>
<input name="email" placeholder="Email" />
{state.errors?.email && (
<span style={{ color: "red" }}>{state.errors.email}</span>
)}
</div>
<div>
<input name="password" type="password" placeholder="Пароль" />
{state.errors?.password && (
<span style={{ color: "red" }}>{state.errors.password}</span>
)}
</div>
<button type="submit">Зарегистрироваться</button>
{state.success && <div style={{ color: "green" }}>{state.success}</div>}
</form>
);
}
Обратите внимание: ошибки для каждого поля выводятся рядом с соответствующим input. После успешной отправки появляется сообщение об успехе.
4. Под капотом: как работает useFormState
Маленькое лирическое отступление для любознательных.
Когда вы используете useFormState, Next.js сам заботится о:
- Передаче состояния между клиентом и сервером.
- Ре-рендере компонента после получения ответа от Server Action.
- Безопасности: состояния не могут быть подделаны на клиенте.
Важно: useFormState работает только в client components (компонентах с "use client").
5. Сброс формы после успешной отправки
Частый вопрос: как очистить поля формы после успешной отправки? useFormState сам не сбрасывает значения input'ов, потому что они неконтролируемые (uncontrolled). Нужно сделать это вручную.
Один из вариантов — использовать ref:
import { useRef } from "react";
import { useFormState } from "react-dom";
import { sendMessage } from "../actions/sendMessage";
export default function ContactForm() {
const [state, formAction] = useFormState(sendMessage, {});
const formRef = useRef();
// Сбросить форму после успешной отправки
React.useEffect(() => {
if (state.success && formRef.current) {
formRef.current.reset();
}
}, [state.success]);
return (
<form ref={formRef} action={formAction}>
<textarea name="message" placeholder="Ваше сообщение" />
<button type="submit">Отправить</button>
{state.error && <div style={{ color: "red" }}>{state.error}</div>}
{state.success && <div style={{ color: "green" }}>{state.success}</div>}
</form>
);
}
6. Практические советы и типовые сценарии
Когда использовать useFormState
- Когда нужна серверная валидация и обработка ошибок.
- Когда важно отобразить результат действия сразу в UI.
- Для простых форм, которые не требуют сложного управления локальным состоянием.
Когда не стоит использовать useFormState
- Если форма полностью работает на клиенте (например, сложная валидация "на лету").
- Если нужно управлять каждым полем формы вручную (controlled inputs).
Советы по организации состояния
- Возвращайте из Server Action только нужные данные (ошибки, сообщения, поля).
- Не бойтесь возвращать сложные объекты — useFormState всё корректно обработает.
- Для сложных форм используйте вложенные объекты для ошибок: { errors: { email: "...", password: "..." } }.
Обработка ошибок, связанных с сервером
Иногда на сервере может произойти "непредвиденная неприятность": база данных недоступна, внешний сервис не отвечает и т.д. Вы можете вернуть ошибку в state или выбросить исключение.
Рекомендуется: возвращать ошибки как часть состояния, чтобы пользователь мог их увидеть.
export async function sendMessage(prevState, formData) {
try {
// ...ваша логика
} catch (err) {
return { error: "Что-то пошло не так. Попробуйте позже." };
}
}
7. Типичные ошибки при работе с useFormState
Ошибка №1: попытка использовать useFormState в Server Component.
useFormState работает только в компонентах с "use client". Если забыли эту директиву — будет ошибка.
Ошибка №2: неправильный возврат данных из Server Action.
Если серверная функция возвращает не объект, а, например, строку — на клиенте будет не то, что вы ожидаете. Всегда возвращайте объект с нужными полями.
Ошибка №3: забыли передать formAction в атрибут action формы.
Если написать <form action={serverAction}>, а не <form action={formAction}>, useFormState не будет работать как надо.
Ошибка №4: полагаетесь на автоматический сброс полей формы.
useFormState не сбрасывает значения input'ов автоматически. Если нужно — делайте это вручную через ref и reset.
Ошибка №5: забыли обработать ошибки.
Если не выводить сообщения из state, пользователь не узнает, что пошло не так.
Ошибка №6: используете controlled inputs без onChange.
Если хотите контролировать значения полей вручную, используйте useState и onChange, но тогда useFormState не поможет с их сбросом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ