Серверные действия (Server Actions) и мутации данных

Серверные действия (Server Actions) — это асинхронные функции, выполняемые на сервере. Они могут использоваться в серверных и клиентских компонентах для обработки отправки форм и мутаций данных в приложениях Next.js.

🎥 Видео: Подробнее о формах и мутациях с серверными действиями → YouTube (10 минут).

Соглашение

Серверное действие можно определить с помощью директивы React "use server". Директиву можно добавить в начало async функции, чтобы пометить её как серверное действие, или в начало отдельного файла, чтобы пометить все экспортируемые функции этого файла как серверные действия.

Серверные компоненты

Серверные компоненты могут использовать директиву "use server" на уровне функции или модуля. Чтобы создать встроенное серверное действие, добавьте "use server" в начало тела функции:

// Серверный компонент
export default function Page() {
  // Серверное действие
  async function create() {
    'use server'

    // ...
  }

  return (
    // ...
  )
}
// Серверный компонент
export default function Page() {
  // Серверное действие
  async function create() {
    'use server'

    // ...
  }

  return (
    // ...
  )
}

Клиентские компоненты

Клиентские компоненты могут импортировать только действия с директивой "use server" на уровне модуля.

Чтобы вызвать серверное действие в клиентском компоненте, создайте новый файл и добавьте директиву "use server" в начало. Все функции в файле будут помечены как серверные действия, которые можно повторно использовать в клиентских и серверных компонентах:

'use server'

export async function create() {
  // ...
}
'use server'

export async function create() {
  // ...
}
import { create } from '@/app/actions'

export function Button() {
  return (
    // ...
  )
}
import { create } from '@/app/actions'

export function Button() {
  return (
    // ...
  )
}

Также можно передать серверное действие в клиентский компонент как пропс:

<ClientComponent updateItem={updateItem} />
app/client-component.jsx
'use client'

export default function ClientComponent({ updateItem }) {
  return <form action={updateItem}>{/* ... */}</form>
}

Поведение

  • Серверные действия можно вызывать с помощью атрибута action в элементе <form>:
    • Серверные компоненты по умолчанию поддерживают прогрессивное улучшение, то есть форма будет отправлена, даже если JavaScript ещё не загрузился или отключен.
    • В клиентских компонентах формы, вызывающие серверные действия, ставят отправку в очередь, если JavaScript ещё не загружен, отдавая приоритет гидрации клиента.
    • После гидрации браузер не обновляется при отправке формы.
  • Серверные действия не ограничены <form> и могут вызываться из обработчиков событий, useEffect, сторонних библиотек и других элементов форм, таких как <button>.
  • Серверные действия интегрируются с архитектурой кэширования и ревалидации Next.js. При вызове действия Next.js может вернуть обновлённый UI и новые данные за один серверный запрос.
  • Внутри действия используют метод POST, и только этот HTTP-метод может их вызывать.
  • Аргументы и возвращаемое значение серверных действий должны быть сериализуемыми React. См. документацию React для списка сериализуемых аргументов и значений.
  • Серверные действия — это функции. Это означает, что их можно повторно использовать в любом месте приложения.
  • Серверные действия наследуют среду выполнения от страницы или лэйаута, в котором они используются.
  • Серверные действия наследуют конфигурацию сегмента маршрута от страницы или лэйаута, в котором они используются, включая такие поля, как maxDuration.

Примеры

Формы

React расширяет HTML-элемент <form>, позволяя вызывать серверные действия с помощью пропса action.

При вызове в форме действие автоматически получает объект FormData. Вам не нужно использовать React useState для управления полями — вместо этого можно извлечь данные с помощью нативных методов FormData:

export default function Page() {
  async function createInvoice(formData: FormData) {
    'use server'

    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }

    // мутация данных
    // ревалидация кэша
  }

  return <form action={createInvoice}>...</form>
}
export default function Page() {
  async function createInvoice(formData) {
    'use server'

    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }

    // мутация данных
    // ревалидация кэша
  }

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

Полезно знать:

Передача дополнительных аргументов

Вы можете передать дополнительные аргументы в серверное действие с помощью метода JavaScript bind.

'use client'

import { updateUser } from './actions'

export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)

  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Обновить имя пользователя</button>
    </form>
  )
}
'use client'

import { updateUser } from './actions'

export function UserProfile({ userId }) {
  const updateUserWithId = updateUser.bind(null, userId)

  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Обновить имя пользователя</button>
    </form>
  )
}

Серверное действие получит аргумент userId в дополнение к данным формы:

app/actions.js
'use server'

export async function updateUser(userId, formData) {
  // ...
}

Полезно знать:

  • Альтернатива — передача аргументов как скрытых полей формы (например, <input type="hidden" name="userId" value={userId} />). Однако значение будет частью отображаемого HTML и не будет закодировано.
  • .bind работает как в серверных, так и в клиентских компонентах. Также поддерживает прогрессивное улучшение.

Состояния ожидания

Вы можете использовать хук React useFormStatus для отображения состояния ожидания во время отправки формы.

  • useFormStatus возвращает статус для конкретного <form>, поэтому должен быть определён как дочерний элемент элемента <form>.
  • useFormStatus — это хук React, поэтому должен использоваться в клиентском компоненте.
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      Добавить
    </button>
  )
}
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      Добавить
    </button>
  )
}

<SubmitButton /> можно вложить в любую форму:

import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'

// Серверный компонент
export default async function Home() {
  return (
    <form action={createItem}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'

// Серверный компонент
export default async function Home() {
  return (
    <form action={createItem}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

Валидация на стороне сервера и обработка ошибок

Рекомендуется использовать HTML-валидацию, такую как required и type="email", для базовой клиентской валидации форм.

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

'use server'

import { z } from 'zod'

const schema = z.object({
  email: z.string({
    invalid_type_error: 'Неверный email',
  }),
})

export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })

  // Возврат при невалидных данных формы
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // Мутация данных
}
'use server'

import { z } from 'zod'

const schema = z.object({
  email: z.string({
    invalid_type_error: 'Неверный email',
  }),
})

export default async function createsUser(formData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })

  // Возврат при невалидных данных формы
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // Мутация данных
}

После валидации полей на сервере можно вернуть сериализуемый объект в действии и использовать хук React useFormState для отображения сообщения пользователю.

  • При передаче действия в useFormState сигнатура функции действия изменяется, чтобы принимать новый параметр prevState или initialState в качестве первого аргумента.
  • useFormState — это хук React, поэтому должен использоваться в клиентском компоненте.
'use server'

export async function createUser(prevState: any, formData: FormData) {
  // ...
  return {
    message: 'Пожалуйста, введите корректный email',
  }
}
'use server'

export async function createUser(prevState, formData) {
  // ...
  return {
    message: 'Пожалуйста, введите корректный email',
  }
}

Затем можно передать действие в хук useFormState и использовать возвращённый state для отображения сообщения об ошибке.

'use client'

import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'

const initialState = {
  message: '',
}

export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button>Зарегистрироваться</button>
    </form>
  )
}
'use client'

import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'

const initialState = {
  message: '',
}

export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button>Зарегистрироваться</button>
    </form>
  )
}

Полезно знать:

Оптимистичные обновления

Вы можете использовать хук React useOptimistic для оптимистичного обновления UI до завершения серверного действия, вместо ожидания ответа:

'use client'

import { useOptimistic } from 'react'
import { send } from './actions'

type Message = {
  message: string
}

export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<
    Message[],
    string
  >(messages, (state, newMessage) => [...state, { message: newMessage }])

  return (
    <div>
      {optimisticMessages.map((m, k) => (
        <div key={k}>{m.message}</div>
      ))}
      <form
        action={async (formData: FormData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Отправить</button>
      </form>
    </div>
  )
}
'use client'

import { useOptimistic } from 'react'
import { send } from './actions'

export function Thread({ messages }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [...state, { message: newMessage }]
  )

  return (
    <div>
      {optimisticMessages.map((m) => (
        <div>{m.message}</div>
      ))}
      <form
        action={async (formData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Отправить</button>
      </form>
    </div>
  )
}

Вложенные элементы

Вы можете вызывать серверное действие в элементах, вложенных в <form>, таких как <button>, <input type="submit"> и <input type="image">. Эти элементы принимают пропс formAction или обработчики событий.

Это полезно, когда нужно вызвать несколько серверных действий в одной форме. Например, можно создать отдельный элемент <button> для сохранения черновика поста в дополнение к его публикации. См. документацию React <form> для получения дополнительной информации.

Программная отправка формы

Вы можете инициировать отправку формы с помощью метода requestSubmit(). Например, когда пользователь нажимает + Enter, вы можете обработать событие onKeyDown:

'use client'

export function Entry() {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (
      (e.ctrlKey || e.metaKey) &&
      (e.key === 'Enter' || e.key === 'NumpadEnter')
    ) {
      e.preventDefault()
      e.currentTarget.form?.requestSubmit()
    }
  }

  return (
    <div>
      <textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
    </div>
  )
}
'use client'

export function Entry() {
  const handleKeyDown = (e) => {
    if (
      (e.ctrlKey || e.metaKey) &&
      (e.key === 'Enter' || e.key === 'NumpadEnter')
    ) {
      e.preventDefault()
      e.currentTarget.form?.requestSubmit()
    }
  }

  return (
    <div>
      <textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
    </div>
  )
}

Это вызовет отправку ближайшего родительского элемента <form>, что запустит Server Action.

Элементы, не являющиеся формами

Хотя Server Actions обычно используются внутри элементов <form>, их также можно вызывать из других частей кода, таких как обработчики событий и useEffect.

Обработчики событий

Вы можете вызывать Server Action из обработчиков событий, таких как onClick. Например, для увеличения счетчика лайков:

'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Всего лайков: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Лайк
      </button>
    </>
  )
}
'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Всего лайков: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Лайк
      </button>
    </>
  )
}

Для улучшения пользовательского опыта рекомендуется использовать другие API React, такие как useOptimistic и useTransition, чтобы обновлять интерфейс до завершения выполнения Server Action на сервере или показывать состояние ожидания.

Вы также можете добавлять обработчики событий к элементам формы, например, для сохранения поля формы при onChange:

app/ui/edit-post.tsx
'use client'

import { publishPost, saveDraft } from './actions'

export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">Опубликовать</button>
    </form>
  )
}

В таких случаях, когда может быть много быстрых последовательных событий, рекомендуется использовать дебаунсинг, чтобы избежать ненужных вызовов Server Action.

useEffect

Вы можете использовать хук React useEffect для вызова Server Action при монтировании компонента или изменении зависимостей. Это полезно для мутаций, которые зависят от глобальных событий или должны запускаться автоматически. Например, onKeyDown для горячих клавиш приложения, хук Intersection Observer для бесконечной прокрутки или при монтировании компонента для обновления счетчика просмотров:

'use client'

import { incrementViews } from './actions'
import { useState, useEffect } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)

  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }

    updateViews()
  }, [])

  return <p>Всего просмотров: {views}</p>
}
'use client'

import { incrementViews } from './actions'
import { useState, useEffect } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)

  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }

    updateViews()
  }, [])

  return <p>Всего просмотров: {views}</p>
}

Не забудьте учесть особенности и ограничения useEffect.

Обработка ошибок

При возникновении ошибки она будет перехвачена ближайшим error.js или границей <Suspense> на клиенте. Рекомендуется использовать try/catch для возврата ошибок и их обработки в интерфейсе.

Например, ваш Server Action может обрабатывать ошибки при создании нового элемента, возвращая сообщение:

'use server'

export async function createTodo(prevState: any, formData: FormData) {
  try {
    // Изменение данных
  } catch (e) {
    throw new Error('Не удалось создать задачу')
  }
}
'use server'

export async function createTodo(prevState, formData) {
  try {
    // Изменение данных
  } catch (e) {
    throw new Error('Не удалось создать задачу')
  }
}

Полезно знать:

Ревалидация данных

Вы можете ревалидировать кеш Next.js внутри Server Actions с помощью API revalidatePath:

'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}

Или инвалидировать конкретный запрос данных с помощью тега кеша, используя revalidateTag:

'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}

Перенаправление

Если вы хотите перенаправить пользователя на другой маршрут после завершения Server Action, вы можете использовать API redirect. redirect должен вызываться вне блока try/catch:

'use server'

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

export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // Обновление кешированных постов
  redirect(`/post/${id}`) // Переход на страницу нового поста
}
'use server'

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

export async function createPost(id) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // Обновление кешированных постов
  redirect(`/post/${id}`) // Переход на страницу нового поста
}

Куки

Вы можете получать, устанавливать и удалять куки внутри Server Action с помощью API cookies:

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  // Получение куки
  const value = cookies().get('name')?.value

  // Установка куки
  cookies().set('name', 'Delba')

  // Удаление куки
  cookies().delete('name')
}
'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  // Получение куки
  const value = cookies().get('name')?.value

  // Установка куки
  cookies().set('name', 'Delba')

  // Удаление куки
  cookies().delete('name')
}

См. дополнительные примеры для удаления куки из Server Actions.

Безопасность

Аутентификация и авторизация

Следует относиться к Server Actions как к общедоступным конечным точкам API и убедиться, что пользователь авторизован для выполнения действия. Например:

app/actions.ts
'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('Для выполнения этого действия необходимо войти в систему')
  }

  // ...
}

Замыкания и шифрование

Определение Server Action внутри компонента создает замыкание, где действие имеет доступ к области видимости внешней функции. Например, действие publish имеет доступ к переменной publishVersion:

export default function Page() {
  const publishVersion = await getLatestVersion();

  async function publish(formData: FormData) {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('Версия изменилась с момента нажатия кнопки публикации');
    }
    ...
  }

  return <button action={publish}>Опубликовать</button>;
}
export default function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('Версия изменилась с момента нажатия кнопки публикации');
    }
    ...
  }

  return <button action={publish}>Опубликовать</button>;
}

Замыкания полезны, когда вам нужно захватить снимок данных (например, publishVersion) во время рендеринга, чтобы использовать их позже при вызове действия.

Однако для этого захваченные переменные отправляются на клиент и обратно на сервер при вызове действия. Чтобы предотвратить утечку конфиденциальных данных на клиент, Next.js автоматически шифрует замкнутые переменные. Новый приватный ключ генерируется для каждого действия при каждой сборке приложения Next.js. Это означает, что действия могут быть вызваны только для конкретной сборки.

Полезно знать: Не рекомендуется полагаться только на шифрование для предотвращения утечки конфиденциальных данных на клиент. Вместо этого следует использовать React taint APIs для активного предотвращения отправки определенных данных на клиент.

Переопределение ключей шифрования (продвинутый уровень)

При самостоятельном размещении приложения Next.js на нескольких серверах каждый экземпляр сервера может получить разные ключи шифрования, что может привести к несоответствиям.

Чтобы избежать этого, вы можете переопределить ключ шифрования с помощью переменной окружения process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY. Указание этой переменной гарантирует, что ваши ключи шифрования остаются постоянными между сборками, и все серверные экземпляры используют один и тот же ключ.

Это продвинутый сценарий, где критически важна согласованность поведения шифрования между разными развертываниями. Следует учитывать стандартные практики безопасности, такие как ротация ключей и подпись.

Полезно знать: Приложения Next.js, развернутые на Vercel, обрабатывают это автоматически.

Разрешенные источники (продвинутый уровень)

Поскольку Server Actions могут быть вызваны в элементе <form>, они подвержены CSRF-атакам.

Внутри Server Actions используют метод POST, и только этот HTTP-метод разрешен для их вызова. Это предотвращает большинство уязвимостей CSRF в современных браузерах, особенно с учетом того, что SameSite cookies используются по умолчанию.

В качестве дополнительной защиты Server Actions в Next.js также сравнивают заголовок Origin с заголовком Host (или X-Forwarded-Host). Если они не совпадают, запрос будет прерван. Другими словами, Server Actions могут быть вызваны только с того же хоста, что и страница, на которой они размещены.

Для крупных приложений, использующих обратные прокси или многоуровневые бэкенд-архитектуры (где серверный API отличается от продакшен-домена), рекомендуется использовать опцию конфигурации serverActions.allowedOrigins для указания списка безопасных источников. Опция принимает массив строк.

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

Узнайте больше о Безопасности и Server Actions.

Дополнительные ресурсы

Для получения дополнительной информации о Server Actions см. следующие документы React: