Server Actions в Next.js

Server Actions это асинхронные функции, которые выполняются на сервере. Они позволяют мутировать данные (создавать, обновлять, удалять) напрямую из компонентов без создания отдельных API-эндпоинтов.

Как определить Server Action

Server Action это функция, помеченная директивой 'use server':

// actions/problem.ts
'use server'

import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function solveProblem(problemId: string, userId: string) {
  await db.user.update({
    where: { id: userId },
    data: {
      solvedProblems: { push: problemId }
    }
  })

  revalidatePath('/problems')
}

Директиву можно указать на уровне файла (как выше) или на уровне отдельной функции:

export default async function Page() {
  async function handleSubmit(formData: FormData) {
    'use server'
    const name = formData.get('name')
    await db.user.update({ where: { id: userId }, data: { name } })
  }

  return <form action={handleSubmit}>...</form>
}

Использование с формами

Server Actions можно передать напрямую в action формы. Форма работает даже без JavaScript на клиенте (progressive enhancement):

// app/settings/page.tsx
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export default async function SettingsPage() {
  async function updateProfile(formData: FormData) {
    'use server'
    const name = formData.get('name') as string

    await db.user.update({
      where: { id: currentUserId },
      data: { name }
    })

    revalidatePath('/settings')
  }

  return (
    <form action={updateProfile}>
      <input name="name" placeholder="Имя на Hack Frontend" />
      <button type="submit">Сохранить</button>
    </form>
  )
}

Использование с useTransition

Для оптимистичных обновлений и индикации загрузки:

'use client'

import { useTransition } from 'react'
import { solveProblem } from '@/actions/problem'

export function SolveButton({
  problemId,
  userId
}: {
  problemId: string
  userId: string
}) {
  const [isPending, startTransition] = useTransition()

  return (
    <button
      disabled={isPending}
      onClick={() => {
        startTransition(() => solveProblem(problemId, userId))
      }}
    >
      {isPending ? 'Проверка...' : 'Решил'}
    </button>
  )
}

Валидация данных

Server Actions получают пользовательский ввод, поэтому валидация обязательна:

'use server'

import { z } from 'zod'

const schema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email()
})

export async function updateUser(formData: FormData) {
  const result = schema.safeParse({
    name: formData.get('name'),
    email: formData.get('email')
  })

  if (!result.success) {
    return { error: result.error.flatten().fieldErrors }
  }

  await db.user.update({
    where: { id: currentUserId },
    data: result.data
  })

  revalidatePath('/settings')
  return { success: true }
}

Ревалидация после мутации

После изменения данных нужно обновить кеш:

'use server'

import { revalidatePath, revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createProblem(data: ProblemData) {
  await db.problem.create({ data })

  revalidateTag('problems')   // инвалидирует Data Cache по тегу
  revalidatePath('/problems') // инвалидирует Full Route Cache
  redirect('/problems')       // перенаправляет после создания
}

На собеседовании:

Server Actions не замена REST API. Они предназначены для мутаций данных из UI. Для публичного API, интеграций с другими сервисами или webhook'ов используй Route Handlers.

Полезные ресурсы

Связанные темы