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

Sources

Related topics