Что такое Server Actions в Next.js-подобных React-фреймворках и как их использовать?
Что это такое
Server Actions — это функции, которые физически выполняются только на сервере, но их можно импортировать и вызывать из клиентского кода так, будто это обычный JS. Фреймворк (Next.js App Router, Remix-подобные решения) под капотом превращает такой вызов в сетевой запрос (RPC поверх POST), сериализует аргументы и результат, и гарантирует, что тело функции никогда не попадёт в клиентский бандл.
Объявляются директивой "use server" — либо в начале файла (все экспортируемые функции становятся actions), либо прямо внутри функции, объявленной в серверном компоненте.
Зачем они нужны
- Убирают необходимость вручную писать
route.ts/API-эндпоинт под каждую мутацию. - Дают типобезопасность end-to-end: сигнатура функции — это и есть контракт.
- Поддерживают progressive enhancement — форма с
action={serverAction}работает даже без JS, потому что браузер делает обычный submit. - Интегрируются с кэшем фреймворка:
revalidatePath/revalidateTagпозволяют точечно инвалидировать данные после мутации.
Как использовать
- В отдельном файле — удобно для переиспользуемых actions:
// actions/todos.ts
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
export async function addTodo(formData: FormData) {
const title = formData.get("title");
if (typeof title !== "string" || title.trim().length === 0) {
return { error: "Название обязательно" };
}
await db.todo.create({ data: { title } });
revalidatePath("/todos"); // сбрасываем кэш страницы со списком
}
- Инлайново в серверном компоненте:
export default function TodosPage() {
async function addTodo(formData: FormData) {
"use server";
// логика мутации
}
return (
<form action={addTodo}>
<input name="title" />
<button type="submit">Добавить</button>
</form>
);
}
- Вызов вне форм — из клиентского компонента как обычная async-функция, например по клику, с
useTransitionдля отображения состояния загрузки, иuseActionState(бывшийuseFormState) для отображения ошибок валидации без лишнего client state.
Что важно на senior-уровне
- Безопасность: action — это фактически публичный HTTP-эндпоинт. Внутри обязательно нужно проверять авторизацию и права доступа, а не полагаться на то, что кнопка скрыта в UI.
- Валидация на сервере — данные от клиента нельзя доверять; входные аргументы нужно валидировать (например, через zod), включая случай прямого вызова action в обход формы.
- Сериализация — аргументы и возврат должны быть сериализуемыми (без функций, классов, циклических ссылок).
- Кэш и ревалидация — нужно понимать разницу между
revalidatePathиrevalidateTag, иначе после мутации UI покажет устаревшие данные. - Идемпотентность и повторные сабмиты — формы могут быть отправлены повторно (двойной клик, ретрай сети), стоит закладывать защиту от дублей на уровне БД/бизнес-логики.
Ограничения
Server Actions — не замена полноценному REST/GraphQL API для внешних клиентов (мобильных приложений, сторонних интеграций): это внутренний механизм связи UI с сервером конкретного фреймворка.
Что хочет услышать интервьюер
Понимание, что код action выполняется только на сервере и не попадает в клиентский бандл
Знание директивы "use server" и вариантов её размещения (файл целиком vs функция внутри компонента)
Понимание интеграции с формами и progressive enhancement (работа без JS)
Осознание, что action — это публичная точка входа, требующая проверки авторизации и валидации на сервере
Знание механизмов инвалидации кэша после мутации (revalidatePath/revalidateTag)
Пример: Клиентский вызов action с useActionState и useTransition
"use client";
import { useActionState } from "react";
import { addTodo } from "@/actions/todos";
const initialState = { error: null };
export function AddTodoForm() {
const [state, formAction, isPending] = useActionState(addTodo, initialState);
return (
<form action={formAction}>
<input name="title" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? "Добавляем..." : "Добавить"}
</button>
{state?.error && <p role="alert">{state.error}</p>}
</form>
);
}
Пример: Проверка авторизации внутри action
"use server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
export async function deletePost(postId: string) {
const session = await auth();
if (!session?.user) {
throw new Error("Требуется авторизация");
}
const post = await db.post.findUnique({ where: { id: postId } });
// проверяем, что удаляет владелец, а не любой залогиненный пользователь
if (!post || post.authorId !== session.user.id) {
throw new Error("Недостаточно прав");
}
await db.post.delete({ where: { id: postId } });
}
Типичные ошибки
Считать, что раз action вызывается 'как функция', проверять права доступа внутри неё не нужно
Не валидировать входные данные на сервере, полагаясь на валидацию формы на клиенте
Забывать вызвать revalidatePath/revalidateTag и удивляться, что UI показывает устаревшие данные после мутации
Пытаться передавать в action несериализуемые данные (функции, экземпляры классов, DOM-элементы)
Путать Server Actions с обычными API-роутами и пытаться дергать их напрямую по URL из внешних клиентов


