1. Типичные ошибки при работе с useFormState
useFormState — это отличный инструмент, но он требует понимания, как именно он взаимодействует с вашим Server Action.
Ошибка №1: Забыли await при вызове Server Action
Проблема: Вы объявляете Server Action как async функцию, но когда используете её в useFormState, забываете про await. В итоге useFormState получает не результат выполнения Server Action, а... промис. А промис, как вы помните, это просто обещание, а не готовое значение.
Почему происходит: Мы привыкли, что React-хуки работают "мгновенно". Но Server Actions — это асинхронные операции, и им нужно время.
Как исправить: Убедитесь, что ваш Server Action, переданный в useFormState, является асинхронным, и внутри формы вы его используете корректно.
// app/actions/taskActions.ts (Server Action)
"use server";
export async function createTask(prevState: any, formData: FormData) {
const title = formData.get('title') as string;
if (title.length < 3) {
return { message: 'Заголовок задачи должен быть длиннее 3 символов', success: false };
}
// Имитация задержки сервера
await new Promise(resolve => setTimeout(resolve, 500));
console.log(`Создана задача: ${title}`);
return { message: `Задача "${title}" успешно создана!`, success: true };
}
// app/dashboard/page.tsx (Client Component, или Server Component, если форма внутри)
"use client"; // Важно, если компонент клиентский
import { useFormState } from 'react-dom';
import { createTask } from '../actions/taskActions'; // Импортируем Server Action
export default function NewTaskForm() {
// initialState - начальное состояние формы, action - Server Action
const [state, formAction] = useFormState(createTask, { message: '', success: false });
return (
<form action={formAction} className="space-y-4 p-4 border rounded shadow">
<h2 className="text-xl font-bold">Добавить новую задачу</h2>
<div>
<label htmlFor="taskTitle" className="block text-sm font-medium text-gray-700">Название задачи:</label>
<input
type="text"
id="taskTitle"
name="title"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Добавить
</button>
{state.message && (
<p className={`mt-2 text-sm ${state.success ? 'text-green-600' : 'text-red-600'}`}>
{state.message}
</p>
)}
</form>
);
}
В данном случае await не пишется напрямую в action={formAction}, потому что useFormState сам оборачивает Server Action и корректно дожидается его выполнения. Ошибка "забыли await" скорее относится к ситуациям, когда вы пытаетесь вызвать Server Action вручную внутри useEffect или другого обработчика события, не связанного напрямую с action формы. Но для useFormState это не актуально.
Ошибка №2: Неправильное обновление состояния или мутация prevState
Проблема: Внутри вашего Server Action, когда вы возвращаете новое состояние, вы можете случайно мутировать (mutate) prevState напрямую, вместо того чтобы возвращать новый объект. Или вы возвращаете неполный объект, который не содержит всех нужных свойств.
Почему происходит: React, а вместе с ним и хуки вроде useFormState, полагаются на то, что вы не будете напрямую изменять объекты состояния. Вместо этого нужно всегда возвращать новый объект с обновленными данными. Это касается и prevState, который передаётся в Server Action.
Как исправить: Всегда создавайте новый объект для возвращаемого состояния, используя оператор ... (spread operator), чтобы скопировать предыдущее состояние и обновить только нужные поля.
// Плохо: мутация prevState
export async function updateTask(prevState, formData) {
// prevState.message = 'Ошибка!'; // Так делать нельзя!
// return prevState;
return { ...prevState, message: 'Ошибка!' }; // Правильно: создаем новый объект
}
Ошибка №3: Состояние useFormState используется не для результата действия
Проблема: Иногда разработчики путают useFormState с обычным useState и пытаются хранить в нём любое состояние формы (например, значения полей ввода).
Почему происходит: useFormState специально разработан для получения результата выполнения Server Action. Его state — это то, что ваш Server Action вернул. Для управления значениями полей ввода в контролируемых компонентах вам по-прежнему нужен useState или другие подходы.
Как исправить: Используйте useFormState только для получения и отображения ответов от Server Action (сообщения об успехе/ошибке, валидационные данные). Для других элементов формы (вроде динамических значений полей) используйте стандартные подходы React.
// Пример использования useState для поля ввода и useFormState для результата
"use client";
import { useFormState } from 'react-dom';
import { createTask } from '../actions/taskActions';
import { useState } from 'react'; // Импортируем useState
export default function NewTaskFormWithControlledInput() {
const [state, formAction] = useFormState(createTask, { message: '', success: false });
const [taskTitle, setTaskTitle] = useState(''); // Используем useState для значения поля
// Обработчик изменения поля ввода
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTaskTitle(e.target.value);
};
return (
<form action={formAction} className="space-y-4 p-4 border rounded shadow">
<h2 className="text-xl font-bold">Добавить новую задачу (контролируемое поле)</h2>
<div>
<label htmlFor="taskTitleControlled" className="block text-sm font-medium text-gray-700">Название задачи:</label>
<input
type="text"
id="taskTitleControlled"
name="title"
value={taskTitle} // Привязываем значение к состоянию useState
onChange={handleTitleChange} // Обработчик изменения
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<button type="submit" className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700">
Добавить
</button>
{state.message && (
<p className={`mt-2 text-sm ${state.success ? 'text-green-600' : 'text-red-600'}`}>
{state.message}
</p>
)}
</form>
);
}
Ошибка №4: Несоответствие типа для initialState
Проблема: useFormState ожидает, что начальное состояние (initialState) будет того же типа, что и объект, который возвращает ваш Server Action. Если они не совпадают, TypeScript будет ругаться, а JavaScript может работать непредсказуемо.
Как исправить: Убедитесь, что initialState полностью соответствует структуре данных, которую вы ожидаете получить от Server Action.
// app/actions/userActions.ts
"use server";
interface UserActionState {
message: string;
errors?: {
username?: string;
email?: string;
};
}
export async function registerUser(prevState: UserActionState, formData: FormData): Promise<UserActionState> {
const username = formData.get('username') as string;
const email = formData.get('email') as string;
const errors: UserActionState['errors'] = {};
if (username.length < 5) {
errors.username = "Имя пользователя должно быть длиннее 4 символов.";
}
if (!email.includes('@')) {
errors.email = "Некорректный формат email.";
}
if (Object.keys(errors).length > 0) {
return { message: 'Ошибка валидации', errors };
}
// Имитация регистрации
await new Promise(resolve => setTimeout(resolve, 500));
return { message: `Пользователь ${username} зарегистрирован!`, errors: {} };
}
// app/register/page.tsx
"use client";
import { useFormState } from 'react-dom';
import { registerUser } from '../actions/userActions';
export default function RegisterForm() {
// Начальное состояние точно соответствует интерфейсу UserActionState
const initialState = {
message: '',
errors: {}
};
const [state, formAction] = useFormState(registerUser, initialState);
return (
<form action={formAction}>
{/* Поля формы для username, email */}
{state.errors?.username && <p className="text-red-500">{state.errors.username}</p>}
{state.errors?.email && <p className="text-red-500">{state.errors.email}</p>}
{state.message && <p className="text-blue-500">{state.message}</p>}
<button type="submit">Зарегистрироваться</button>
</form>
);
}
2. Типичные ошибки при работе с useOptimistic
useOptimistic — это мощный инструмент для улучшения пользовательского опыта, но он добавляет слой сложности, и ошибки тут тоже не редкость.
Ошибка №1: Неправильное обновление оптимистичного состояния (мутация вместо создания нового)
Проблема: Как и с useFormState (и вообще с любым состоянием в React), нельзя напрямую изменять объект, который является оптимистичным состоянием. Вы должны возвращать новый объект.
Почему происходит: React отслеживает изменения состояния, сравнивая ссылки на объекты. Если вы мутируете существующий объект, ссылка не меняется, и React думает, что состояние не обновилось, не вызывая ре-рендера.
Как исправить: Всегда создавайте новый объект или массив при обновлении оптимистичного состояния. Используйте spread-операторы или методы массивов, которые возвращают новые массивы (например, map, filter).
// Плохо (мутация):
const updateOptimistic = (currentTasks, payload) => {
const taskToUpdate = currentTasks.find(t => t.id === payload.id);
if (taskToUpdate) taskToUpdate.completed = payload.completed;
return currentTasks; // Возвращаем тот же объект, React не увидит изменения
};
// Хорошо (создание нового объекта/массива):
const updateOptimistic = (currentTasks, payload) => {
return currentTasks.map(task =>
task.id === payload.id
? { ...task, completed: payload.completed } // Создаем новый объект задачи
: task
);
};
Ошибка №2: Отсутствие addOptimistic для всех возможных состояний
Проблема: Вы настроили useOptimistic для успешного сценария (например, добавления элемента), но забыли учесть сценарий ошибки или отката. Если сервер вернёт ошибку, ваш оптимистичный UI может остаться в неправильном состоянии.
Почему происходит: useOptimistic изменяет UI до подтверждения сервера. Если подтверждение не приходит или приходит с ошибкой, вам нужно, чтобы UI вернулся в исходное состояние.
Как исправить: Убедитесь, что ваш Server Action после реального выполнения возвращает актуальное состояние (или ошибку), и UI корректно обрабатывает этот ответ, откатывая оптимистичное изменение при необходимости. Обычно это делается с помощью revalidatePath или revalidateTag в Server Action, которые заставляют Next.js перерендерить нужные данные.
// app/actions/taskActions.ts
"use server";
import { revalidatePath } from 'next/cache';
export async function toggleTaskCompletion(id: number, completed: boolean) {
// Имитация ошибки 20% случаев
if (Math.random() < 0.2) {
console.error("Имитация ошибки сервера");
// В реальном приложении здесь будет выброшена ошибка или возвращено состояние ошибки
return { success: false, message: "Не удалось обновить задачу." };
}
await new Promise(resolve => setTimeout(resolve, 500));
console.log(`Задача ${id} обновлена: завершена = ${completed}`);
revalidatePath('/dashboard'); // Заставит Next.js обновить данные на /dashboard
return { success: true, message: "Задача успешно обновлена." };
}
// app/dashboard/tasks.tsx (Client Component)
"use client";
import { useOptimistic } from 'react';
import { toggleTaskCompletion } from '../actions/taskActions';
interface Task {
id: number;
title: string;
completed: boolean;
}
export function TaskList({ initialTasks }: { initialTasks: Task[] }) {
const [optimisticTasks, addOptimistic] = useOptimistic(
initialTasks,
(currentTasks: Task[], taskId: number) => {
// Оптимистичное обновление: меняем состояние задачи
return currentTasks.map(task =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
);
}
);
const handleToggle = async (taskId: number, currentCompleted: boolean) => {
addOptimistic(taskId); // Мгновенно обновляем UI
// Вызываем Server Action
const result = await toggleTaskCompletion(taskId, !currentCompleted);
// В этом простом примере revalidatePath позаботится об откате, если Server Action выбросит ошибку,
// но для более сложного отката можно использовать useFormState или обработать результат напрямую
if (!result.success) {
alert(result.message); // Показываем ошибку пользователю
// Здесь может быть дополнительная логика для отката, если revalidatePath недостаточно
}
};
return (
<ul className="space-y-2">
{optimisticTasks.map(task => (
<li key={task.id} className="flex items-center space-x-2">
<input
type="checkbox"
checked={task.completed}
onChange={() => handleToggle(task.id, task.completed)}
className="form-checkbox h-5 w-5 text-blue-600"
/>
<span className={task.completed ? 'line-through text-gray-500' : 'text-gray-900'}>
{task.title}
</span>
</li>
))}
</ul>
);
}
Ошибка №3: Слишком сложная логика в addOptimistic
Проблема: Функция addOptimistic должна быть легковесной и быстрой. Она не должна выполнять сложные вычисления, сетевые запросы или иметь побочные эффекты.
Почему происходит: Эта функция вызывается синхронно и мгновенно, чтобы обновить UI. Если она будет медленной, вы потеряете преимущество оптимистичного обновления.
Как исправить: Перенесите всю сложную логику, работу с API, валидацию и т.д. в ваш Server Action. addOptimistic должна только быстро модифицировать локальное состояние, чтобы оно соответствовало предполагаемому результату Server Action.
Ошибка №4: Несоответствие типов между реальным и оптимистичным состоянием
Проблема: useOptimistic принимает начальное состояние и функцию updateOptimistic, которая возвращает новое оптимистичное состояние. Типы этих состояний должны быть совместимы. Если ваш Server Action возвращает данные одного типа, а addOptimistic пытается представить их в другом, возникнут ошибки.
Как исправить: Тщательно типизируйте свои данные и убедитесь, что функция updateOptimistic возвращает данные того же формата, что и ваше initialState.
// Если initialTasks: Task[], то и updateOptimistic должна возвращать Task[]
// Пример из Ошибки №2 уже демонстрирует правильный подход.
Ошибка №5: useOptimistic используется вне Client Component
Проблема: useOptimistic — это React Hook. Как и все React Hooks, он может быть использован только в клиентских компонентах (тех, что помечены "use client"). Если вы попытаетесь использовать его в Server Component, получите ошибку.
Почему происходит: Server Components рендерятся на сервере и не имеют доступа к состоянию React или хукам.
Как исправить: Всегда используйте useOptimistic (и useFormState, useState, useEffect и т.д.) только в клиентских компонентах. Если вам нужен оптимистичный UI для формы, которая лежит в Server Component, вынесите эту форму в отдельный Client Component.
// app/dashboard/page.tsx (Server Component)
import { TaskList } from './tasks'; // Импортируем клиентский компонент
async function getTasks() {
// Получаем задачи с сервера
return [{ id: 1, title: 'Купить молоко', completed: false }];
}
export default async function DashboardPage() {
const tasks = await getTasks(); // Получаем данные на сервере
return (
<div>
<h1>Мои задачи</h1>
<TaskList initialTasks={tasks} /> {/* Передаем данные в клиентский компонент */}
</div>
);
}
// app/dashboard/tasks.tsx (Client Component - здесь useOptimistic)
"use client";
// ... код TaskList из примера выше
3. Общие подводные камни Server Actions и хуков форм
Помимо специфических ошибок useFormState и useOptimistic, есть несколько общих моментов, которые часто вызывают затруднения.
Ошибка №1: Использование Server Actions без форм (или вне форм) неправильно
Проблема: Server Actions в Next.js 15 тесно интегрированы с HTML-формами. Хотя вы можете вызвать их напрямую, их основной сценарий использования — это обработка отправки <form action={serverAction}>. Попытка вызывать их "как обычные API-вызовы" из useEffect или других мест может быть менее идиоматичной или привести к сложностям с кешированием и ревалидацией.
Почему происходит: Next.js оптимизирует работу Server Actions с формами, автоматически обрабатывая formData, ожидание завершения действия и ревалидацию кеша.
Как исправить: По возможности используйте Server Actions с тегом <form>. Если вам нужна серверная логика, не связанная с формой (например, получение данных для клиентского компонента), рассмотрите использование Route Handlers (API-маршрутов). Это более явный способ для создания REST-подобных API-эндпоинтов.
Ошибка №2: Забыли "use server" или export async function
Проблема: Ваша Server Action просто не работает, а вы не понимаете почему. Проблема может быть в отсутствии директивы "use server" в начале файла или перед функцией, или в том, что функция не export async.
Почему происходит: Next.js использует эти маркеры для идентификации функций, которые должны быть обработаны как Server Actions. Без них это просто обычные JavaScript-функции.
Как исправить: Проверьте, что в самом начале файла (или перед каждой Server Action функцией) стоит "use server"; и что функция объявлена как export async function названиеФункции() { ... }.
// app/actions/something.ts
"use server"; // В самом начале файла!
export async function myServerAction(formData) {
// ... ваш код Server Action
}
// Или так, если несколько Server Actions в одном файле и хотите по отдельности:
export async function anotherServerAction(formData) {
"use server"; // Прямо в функции
// ...
}
Ошибка №3: Недостаточное использование клиентских индикаторов загрузки
Проблема: Вы реализовали оптимистичный UI с useOptimistic, но что, если Server Action зависнет надолго или происходит сложная операция? Пользователь видит мгновенное обновление, но не знает, что на фоне что-то ещё происходит.
Почему происходит: Оптимистичный UI скрывает задержку, но не отменяет её. Пользователь ожидает подтверждения того, что действие "ушло".
Как исправить: Используйте useFormStatus (для форм) или обычный useState (для других взаимодействий) вместе с индикаторами загрузки (спиннеры, блокировка кнопки). Это даст пользователю понять, что система работает.
"use client";
import { useFormStatus } from 'react-dom'; // Хук для статуса формы
function SubmitButton() {
const { pending } = useFormStatus(); // Получаем статус отправки формы
return (
<button type="submit" disabled={pending}>
{pending ? 'Отправляем...' : 'Отправить'}
</button>
);
}
// В компоненте формы:
// <form action={formAction}>
// ...
// <SubmitButton /> // Внутри формы
// </form>
Ошибка №4: Смешение Server Actions и Route Handlers без понимания
Проблема: Иногда возникает путаница, когда использовать Server Actions, а когда — Route Handlers (файлы route.ts в папке api).
Почему происходит: Оба механизма позволяют запускать серверный код, но предназначены для разных сценариев.
Как исправить:
- Server Actions: Идеальны для мутаций данных (CRUD), запускаемых непосредственно из UI (часто из <form>), когда вам нужна глубокая интеграция с Next.js кешированием и ревалидацией. Они тесно связаны с React компонентами.
- Route Handlers (API Routes): Подходят для создания полноценных REST-подобных API-эндпоинтов, которые могут быть вызваны любым клиентом (не только вашим Next.js-приложением). Если вам нужна полноценная валидация тела запроса, сложная авторизация, или если ваш фронтенд на другом фреймворке, то это ваш выбор.
Представьте, что Server Actions — это такие "быстрые кнопки" в вашем приложении, которые напрямую вызывают серверную логику для конкретных UI-действий. А Route Handlers — это полноценные "двери" в ваш бэкенд, через которые могут ходить все, кто знает адрес.
Надеюсь, этот разбор типичных ошибок поможет вам избежать большинства проблем и сделает вашу работу с Next.js 15 и его новыми хуками форм гораздо приятнее и продуктивнее! Успехов в кодировании!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ