Next.jsMiddleTechnical
Что такое Server Actions в Next.js и как они работают?
Server Actions — async-функции с директивой 'use server', которые выполняются на сервере и могут вызываться из форм или клиентского кода без явного API-эндпоинта.
Server Actions в Next.js
Server Actions — это асинхронные функции, помеченные директивой "use server", которые Next.js автоматически превращает в HTTP POST-эндпоинты. Их можно вызывать напрямую из JSX-форм или из клиентского кода — Next.js сам формирует сетевой запрос, сериализует аргументы через React Flight Protocol и выполняет функцию на сервере.
Как объявить Server Action
Директива "use server" ставится либо в начало файла (тогда все экспорты файла — Server Actions), либо внутри тела конкретной функции:
// app/actions/user.ts — файл целиком серверный
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
if (!name || !email) {
return { error: "Имя и email обязательны" };
}
await db.query(
"INSERT INTO users (name, email) VALUES ($1, $2)",
[name, email]
);
revalidatePath("/users"); // инвалидируем кэш страницы
return { success: true };
}
Использование в HTML-форме (прогрессивное улучшение)
// app/users/new/page.tsx — Server Component
import { createUser } from "@/app/actions/user";
export default function NewUserPage() {
return (
<form action={createUser}>
<input name="name" placeholder="Имя" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Создать</button>
</form>
);
}
Использование из Client Component с useActionState
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions/user";
export function CreateUserForm() {
const [state, formAction, isPending] = useActionState(createUser, null);
return (
<form action={formAction}>
<input name="name" placeholder="Имя" />
<input name="email" type="email" placeholder="Email" />
{state?.error && <p style={{ color: "red" }}>{state.error}</p>}
{state?.success && <p>Пользователь создан!</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Создание..." : "Создать"}
</button>
</form>
);
}
Вызов как обычная функция из обработчика события
"use client";
import { deleteUser } from "@/app/actions/user";
export function DeleteButton({ userId }: { userId: string }) {
return (
<button
onClick={async () => {
await deleteUser(userId);
}}
>
Удалить
</button>
);
}
Встроенный Server Action внутри Server Component
// app/posts/[id]/page.tsx
export default function PostPage({ params }: { params: { id: string } }) {
async function deletePost() {
"use server";
await db.query("DELETE FROM posts WHERE id = $1", [params.id]);
revalidatePath("/posts");
}
return (
<form action={deletePost}>
<button type="submit">Удалить пост</button>
</form>
);
}
Подводные камни
- Отсутствие валидации — критическая уязвимость: Server Actions доступны как публичные POST-эндпоинты; всегда валидируйте входные данные с помощью zod или аналога и проверяйте авторизацию пользователя.
- FormData, не JSON: когда Action вызывается через
<form action=>, аргументом приходитFormData, а не объект; при вызове как функция — аргументы передаются напрямую. - Нельзя возвращать несериализуемые значения: ответ Action сериализуется через RSC Protocol; Promise, Map, Set и классовые экземпляры вызовут ошибку.
- revalidatePath/revalidateTag работают только на сервере: не пытайтесь вызвать их в Client Component — они определены только в серверном контексте.
- Оптимистичные обновления требуют useOptimistic: без него UI блокируется на время запроса; используйте
useOptimisticиз React для мгновенного отклика. - Ошибки в Actions не перехватываются автоматически: необброшенное исключение показывает глобальный error boundary; возвращайте объекты с полем
errorдля graceful-обработки. - CSRF-защита встроена, но только для same-origin запросов; при работе через iframe или сторонний домен нужна дополнительная защита.
- redirect() внутри try/catch ломается:
redirect()изnext/navigationбросает специальный Error; перехват его в try/catch прервёт редирект — вызывайте redirect вне блока catch.
Common mistakes
- Забыть
revalidatePath/revalidateTagпосле мутации - Возвращать из экшена объекты с секретными полями
- Не валидировать
FormData(доверятьformData.get) - Превышать
bodySizeLimitпри загрузке файлов
What the interviewer is testing
- Знает оба способа объявления
'use server' - Понимает протокол вызова и роль
Next-Action - Умеет инвалидировать кеши после мутации
- Помнит про авторизацию и origin