В классической веб-разработке любая мутация данных (добавление комментария, сохранение профиля, оформление заказа) требовала создания отдельного REST или GraphQL эндпоинта (/api/comments), ручной сериализации в JSON.stringify(), вызова fetch() на клиенте, обработки ошибок сети и сложной инвалидации локального кэша.
В Next.js 16 и React 19 этот громоздкий слой заменяется на Server Actions (Серверные действия):
Асинхронные серверные функции, помеченные директивой 'use server'. Их можно вызывать напрямую из обычных HTML-форм (<form action={myServerAction}>) или из клиентских компонентов как стандартные асинхронные функции JavaScript.
Под капотом Next.js автоматически:
Action-ID.FormData.revalidatePath() или revalidateTag().Взглянем на то, как Server Action объединяет форму, бэкенд и сброс кэша:
+-------------------------------------------------------------------------+
| АРХИТЕКТУРА SERVER ACTIONS В NEXT.JS 16 |
+-------------------------------------------------------------------------+
| БРАУЗЕР (Клиент): |
| <form action={createPost}> |
| <input name="title" /> |
| <button type="submit">Опубликовать</button> |
| </form> |
+-------------------------------------------------------------------------+
| HTTP POST (Автоматический RPC-вызов Next.js) |
| | |
| v |
+-------------------------------------------------------------------------+
| СЕРВЕР (Node.js runtime): |
| async function createPost(formData: FormData) { |
| 'use server'; |
| const title = formData.get('title'); |
| await db.insert(posts).values({ title }); // 1. Запись в БД |
| revalidatePath('/posts'); // 2. Инвалидация кэша |
| } |
+-------------------------------------------------------------------------+
| ОТВЕТ КЛИЕНТУ: |
| Next.js мгновенно возвращает свежий HTML с новым постом в списке! |
| (Никаких ручных refetchQueries() или window.location.reload()!) |
+-------------------------------------------------------------------------+Пользователь нажимает кнопку submit. Данные формы упаковываются в объект FormData.
Next.js отправляет POST-запрос с уникальным идентификатором серверного действия.
Функция с директивой 'use server' валидирует входные данные (Zod) и записывает их в БД.
Next.js сбрасывает закэшированные страницы и возвращает обновленный HTML клиенту.
Какую роль выполняет директива `'use server'` в Next.js 16?
app/api/....httpOnly cookie и редирект на главную.revalidateTag('cart')).Внутри серверного компонента Server Action можно объявить прямо внутри тела файла:
import { revalidatePath } from 'next/cache';
export default function FeedbackPage() {
// Объявляем Server Action прямо здесь!
async function submitFeedback(formData: FormData) {
'use server'; // ОБЯЗАТЕЛЬНО: объявляет серверное действие!
const email = formData.get('email') as string;
const message = formData.get('message') as string;
// Имитация записи в базу данных
console.log(`[Бэкенд] Получен отзыв от ${email}: ${message}`);
// Очищаем кэш страницы отзывов, чтобы показать обновленные данные
revalidatePath('/feedback');
}
return (
<div className="max-w-md mx-auto py-12">
<h1 className="text-2xl font-bold mb-6">Оставить отзыв</h1>
{/* Форма напрямую привязана к серверному действию! */}
<form action={submitFeedback} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-text-muted mb-1">
Ваш Email
</label>
<input
name="email"
type="email"
required
className="w-full px-4 py-2 rounded-lg border border-border bg-bg-surface text-text-primary"
/>
</div>
<div>
<label className="block text-xs font-semibold text-text-muted mb-1">
Сообщение
</label>
<textarea
name="message"
rows={4}
required
className="w-full px-4 py-2 rounded-lg border border-border bg-bg-surface text-text-primary"
/>
</div>
<button
type="submit"
className="w-full py-2.5 px-4 rounded-xl bg-accent-cyan text-bg-base font-semibold hover:opacity-90 transition-opacity"
>
Отправить отзыв
</button>
</form>
</div>
);
}Если форму нужно сделать интерактивной на клиенте ('use client'), Server Actions выносятся в отдельный файл (например, app/actions.ts):
'use server'; // Весь файл экспортирует Server Actions!
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function deletePost(postId: string) {
// 1. Проверка авторизации текущей сессии
// 2. Удаление из базы данных
console.log(`Удален пост: ${postId}`);
// 3. Сброс серверного кэша списка постов
revalidatePath('/posts');
// 4. Опциональный редирект
redirect('/posts');
}useActionState (React 19)#В React 19 для управления состоянием отправки формы используется стандартный хук useActionState:
'use client';
import React, { useActionState } from 'react';
// Серверное действие с возвратом типизированного состояния
async function subscribeAction(prevState: { message: string; success: boolean }, formData: FormData) {
'use server';
const email = formData.get('email');
if (!email || typeof email !== 'string') {
return { success: false, message: 'Некорректный email' };
}
return { success: true, message: 'Вы успешно подписались на рассылку!' };
}
export function SubscribeForm() {
// useActionState возвращает [state, formAction, isPending]
const [state, formAction, isPending] = useActionState(subscribeAction, {
success: false,
message: '',
});
return (
<form action={formAction} className="flex flex-col gap-2">
<div className="flex gap-2">
<input
name="email"
type="email"
placeholder="Ваш email..."
required
disabled={isPending}
className="px-4 py-2 rounded-lg border border-border bg-bg-surface"
/>
<button
type="submit"
disabled={isPending}
className="btn-primary"
>
{isPending ? 'Отправка...' : 'Подписаться'}
</button>
</div>
{state.message && (
<p className={`text-xs ${state.success ? 'text-emerald-400' : 'text-rose-400'}`}>
{state.message}
</p>
)}
</form>
);
}Какую критическую задачу решает вызов функции `revalidatePath('/products')` внутри Server Action после добавления нового товара?
Никогда не доверяйте данным из formData без валидации! Клиент может отправить любые строки.
Решение: Всегда валидируйте входящие параметры через Zod или Valibot внутри Server Action:
const schema = z.object({ email: z.string().email() });
const parsed = schema.safeParse({ email: formData.get('email') });
if (!parsed.success) throw new Error('Невалидный email');Директива 'use server' внутри файла с 'use client' допустима только в качестве отдельного вызова функции, но объявлять inline Server Actions внутри Client Component запрещено.
Решение: Создавайте отдельный файл app/actions.ts с 'use server' в первой строке и импортируйте функции оттуда.