1. Проблемы обновления формы
Если вы уже попробовали реализовать простую форму с Server Actions, то могли заметить: после отправки формы страница не обновляется полностью, но состояние самой формы (например, поля или сообщения об ошибках) не сбрасываются автоматически. А если нужно отобразить результат действия — например, показать пользователю сообщение об успехе или ошибке, или обновить часть интерфейса — тут возникает вопрос: как это сделать красиво и современно?
Вот тут на сцену выходит решение — хук useFormState. Он помогает управлять состоянием формы: хранить результат последнего действия, показывать ошибки, сбрасывать поля, обновлять список задач и так далее. По сути, это современная альтернатива useState, но заточенная под работу с Server Actions.
Вспоминаем, как работает useFormState
useFormState — это React-хук из Next.js, который связывает форму с серверным действием (Server Action). Он позволяет:
- Хранить и отображать результат последнего вызова Server Action (например, ошибки валидации, новые данные и т.д.)
- Передавать начальное состояние (например, "пустая форма" или "список задач")
- Автоматически обновлять интерфейс после отправки формы, не перезагружая страницу
Главная идея: когда пользователь отправляет форму, Server Action возвращает новое состояние, и useFormState его подхватывает. Это похоже на Redux, только проще и без всей этой "магии" с редьюсерами.
2. Базовый пример: форма добавления задачи
Давайте посмотрим на простой пример: у нас есть ToDo-лист, и мы хотим добавлять задачи через форму. После добавления задача должна появиться в списке, а поле ввода — очиститься.
Шаг 1: Создаём серверное действие
// app/actions.js (или app/page.js, если всё в одном файле)
'use server';
export async function addTodo(prevState, formData) {
const title = formData.get('title');
if (!title || title.trim().length < 3) {
// Возвращаем новое состояние с ошибкой
return {
...prevState,
error: 'Название должно быть не короче 3 символов',
};
}
// Добавляем задачу (в реальном приложении — запись в БД)
const newTodos = [...prevState.todos, { title }];
return {
todos: newTodos,
error: null,
};
}
Шаг 2: Используем useFormState в компоненте
import { addTodo } from './actions';
import { useFormState } from 'react-dom';
const initialState = {
todos: [],
error: null,
};
export default function TodoApp() {
// Подключаем useFormState: [state, formAction]
const [state, formAction] = useFormState(addTodo, initialState);
return (
<div>
<h1>Мои задачи</h1>
<form action={formAction}>
<input name="title" placeholder="Новая задача" />
<button type="submit">Добавить</button>
</form>
{/* Выводим ошибку, если есть */}
{state.error && <div style={{ color: 'red' }}>{state.error}</div>}
<ul>
{state.todos.map((todo, idx) => (
<li key={idx}>{todo.title}</li>
))}
</ul>
</div>
);
}
Что здесь происходит?
- useFormState принимает два аргумента: функцию-действие (Server Action) и начальное состояние.
- Возвращает [state, formAction]: актуальное состояние и функцию для атрибута action в форме.
- После отправки формы Server Action возвращает новое состояние, и оно становится доступно в state.
3. Как устроено начальное состояние формы
Начальное состояние — это просто объект (или любой другой тип), который вы передаёте вторым аргументом в useFormState. Обычно это структура, которую вы хотите видеть до первого действия пользователя: например, пустой список задач, пустые поля, отсутствие ошибок.
const initialState = {
todos: [],
error: null,
};
Если вы хотите, чтобы при заходе на страницу в списке уже были какие-то задачи, вы можете получить их на сервере и передать в компонент через пропсы, а потом использовать как initialState.
Как обновляется состояние после действия
Вся магия в том, что после каждой отправки формы Server Action вызывается с текущим состоянием и данными формы. Он возвращает новое состояние, которое тут же попадает в компонент — без перезагрузки страницы и без ручного вызова setState.
Важный момент: Server Action всегда получает на вход сначала предыдущее состояние (prevState), а затем formData. Это позволяет, например, наращивать список задач или возвращать новые ошибки.
Пример с ошибкой
Допустим, пользователь ввёл слишком короткое название задачи:
- Пользователь нажимает "Добавить".
- Server Action видит, что длина меньше 3, и возвращает состояние с ошибкой.
- Компонент мгновенно показывает ошибку, не добавляя задачу.
4. Сброс полей формы после действия
Типичный вопрос: как очистить поля формы после успешного добавления?
- По умолчанию, если поле <input> не управляется через value/controlled-компонент, браузер сам сбрасывает его после submit.
- Если поле управляется через value и useState (controlled), то нужно вручную сбрасывать состояние поля (например, через useRef или useEffect, когда state.todos изменился).
- Для большинства случаев с useFormState достаточно использовать неуправляемый <input> — тогда он очистится сам.
Пример с управляемым полем и сбросом
import { useRef, useEffect } from 'react';
export default function TodoApp() {
const [state, formAction] = useFormState(addTodo, initialState);
const inputRef = useRef(null);
useEffect(() => {
if (!state.error) {
// Если задача добавлена успешно — очищаем поле
inputRef.current.value = '';
}
}, [state.todos, state.error]);
return (
<form action={formAction}>
<input name="title" ref={inputRef} />
<button>Добавить</button>
</form>
);
}
5. Передача начального состояния из Server Component
Часто начальное состояние нужно получить на сервере (например, загрузить список задач из базы данных). В этом случае:
- Получаем данные в Server Component (например, через async function).
- Передаём их как initialState в useFormState.
Пример
// app/page.js (Server Component)
import TodoApp from './TodoApp';
export default async function Page() {
const todos = await getTodosFromDB();
const initialState = { todos, error: null };
return <TodoApp initialState={initialState} />;
}
// app/TodoApp.js (Client Component)
'use client';
import { useFormState } from 'react-dom';
import { addTodo } from './actions';
export default function TodoApp({ initialState }) {
const [state, formAction] = useFormState(addTodo, initialState);
// ...
}
6. Практический пример: форма регистрации с ошибкой
Рассмотрим форму регистрации, где после отправки мы хотим показать ошибку или поздравление с успешной регистрацией. Начальное состояние — пустая ошибка.
// actions.js
'use server';
export async function registerUser(prevState, formData) {
const email = formData.get('email');
if (!email || !email.includes('@')) {
return { error: 'Некорректный email' };
}
// Тут могла бы быть проверка в базе данных...
return { error: null, message: 'Успешная регистрация!' };
}
// RegistrationForm.js
'use client';
import { useFormState } from 'react-dom';
import { registerUser } from './actions';
const initialState = { error: null, message: null };
export default function RegistrationForm() {
const [state, formAction] = useFormState(registerUser, initialState);
return (
<form action={formAction}>
<input name="email" placeholder="Email" />
<button>Зарегистрироваться</button>
{state.error && <div style={{ color: 'red' }}>{state.error}</div>}
{state.message && <div style={{ color: 'green' }}>{state.message}</div>}
</form>
);
}
7. Полезные нюансы
Как работает обновление после действия: “магия” под капотом
Каждый раз, когда вы отправляете форму, Next.js:
- Отправляет данные формы и текущее состояние на сервер (в ваш Server Action).
- Server Action возвращает новое состояние (например, обновлённый список задач, ошибку и т.д.).
- useFormState автоматически обновляет компонент с этим состоянием.
- Если вы используете неуправляемые поля (<input name="..."> без value), браузер сам очищает их после submit.
Важно: useFormState не перезагружает страницу, не вызывает setState вручную и не требует сложной логики. Всё работает “из коробки”.
Когда стоит использовать useFormState
- Когда форма должна показывать результат отправки (например, ошибки, новые данные, сообщения).
- Когда нужно обновить часть интерфейса после действия (например, добавить элемент в список).
- Когда хочется избавиться от ручного состояния useState и всей этой “клиентской” возни.
Если форма совсем простая (например, просто отправить данные и забыть), можно обойтись и без useFormState.
8. Типичные ошибки при работе с useFormState
Ошибка №1: неправильная структура состояния.
Если Server Action возвращает не тот объект, который ожидает компонент, вы получите ошибки. Например, если initialState — это { todos: [], error: null }, а Server Action возвращает просто строку, компонент "расстроится".
Ошибка №2: забыли передать начальное состояние.
Если не передать initialState или передать неправильное, useFormState не сможет корректно отрисовать компонент.
Ошибка №3: попытка использовать useFormState на сервере.
Этот хук работает только в Client Component. Если попробовать использовать его в Server Component, получите ошибку.
Ошибка №4: управляемые поля не сбрасываются.
Если вы используете controlled input с value и useState, поле не очистится само после submit. Нужно сбрасывать вручную (например, через useEffect или useRef).
Ошибка №5: мутирование состояния.
Не мутируйте prevState напрямую в Server Action! Всегда возвращайте новый объект (return { ...prevState, ... }).
Ошибка №6: забыли про асинхронность.
Server Action должен быть async-функцией, чтобы корректно работать с асинхронными операциями (например, запросами к БД).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ