У класичній веб-розробці будь-яка мутація даних (додавання коментаря, збереження профілю, оформлення замовлення) вимагала створення окремого 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' у першому рядку та імпортуйте функції звідти.