Серверные действия и мутации

Серверные действия (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'

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

export async function create() {}
'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Create</button>
}
'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Create</button>
}

Передача действий как пропсов

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

<ClientComponent updateItemAction={updateItem} />
'use client'

export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}
'use client'

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

Обычно плагин TypeScript в Next.js помечал бы updateItemAction в client-component.tsx, поскольку это функция, которая обычно не может быть сериализована между клиентом и сервером. Однако пропсы с именем action или заканчивающиеся на Action предполагают получение серверных действий. Это эвристика, поскольку плагин TypeScript на самом деле не знает, получает ли он серверное действие или обычную функцию. Проверка типов во время выполнения по-прежнему гарантирует, что вы случайно не передадите функцию в клиентский компонент.

Поведение

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

Примеры

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

Хотя серверные действия часто используются внутри элементов <form>, их также можно вызывать с помощью обработчиков событий, таких как 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>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}
'use client'

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

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

  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}

Вы также можете добавить обработчики событий к элементам формы, например, для сохранения поля формы при 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">Publish</button>
    </form>
  )
}

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

useEffect

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

'use client'

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

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

  useEffect(() => {
    startTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])

  // Можно использовать `isPending` для обратной связи с пользователем
  return <p>Total Views: {views}</p>
}
'use client'

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

export default function ViewCount({ initialViews }) {
  const [views, setViews] = useState(initialViews)
  const [isPending, startTransition] = useTransition()

  useEffect(() => {
    starTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])

  // Можно использовать `isPending` для обратной связи с пользователем
  return <p>Total Views: {views}</p>
}

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

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

При возникновении ошибки она будет перехвачена ближайшим error.js или границей <Suspense> на клиенте. Подробнее см. в разделе Обработка ошибок.

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

  • Помимо выброса ошибки, вы также можете вернуть объект для обработки с помощью useActionState.

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

Вы можете ревалидировать кэш Next.js внутри серверных действий с помощью 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')
}

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

Если вы хотите перенаправить пользователя на другой маршрут после завершения серверного действия, вы можете использовать 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}`) // Переход на страницу нового поста
}

Куки

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

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  const cookieStore = await cookies()

  // Получить куки
  cookieStore.get('name')?.value

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

  // Удалить куки
  cookieStore.delete('name')
}
'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  // Получить куки
  const cookieStore = await cookies()

  // Получить куки
  cookieStore.get('name')?.value

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

  // Удалить куки
  cookieStore.delete('name')
}

См. дополнительные примеры удаления куки из серверных действий.

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

По умолчанию, когда серверное действие создаётся и экспортируется, оно создаёт публичную HTTP-конечную точку и должно обрабатываться с теми же предположениями о безопасности и проверками авторизации. Это означает, что даже если серверное действие или служебная функция не импортируются в других местах вашего кода, они всё равно остаются общедоступными.

Для повышения безопасности Next.js имеет следующие встроенные функции:

  • Защищённые идентификаторы действий: Next.js создаёт зашифрованные, недетерминированные идентификаторы, чтобы клиент мог ссылаться и вызывать серверное действие. Эти идентификаторы периодически пересчитываются между сборками для повышения безопасности.
  • Устранение мёртвого кода: Неиспользуемые серверные действия (ссылающиеся на их идентификаторы) удаляются из клиентского бандла, чтобы избежать публичного доступа третьими сторонами.

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

Идентификаторы создаются во время сборки и кэшируются максимум на 14 дней. Они будут перегенерированы при инициировании новой сборки или при инвалидации кэша сборки. Это улучшение безопасности снижает риск в случаях, когда отсутствует слой аутентификации. Однако вы всё равно должны обращаться с серверными действиями как с публичными HTTP-конечными точками.

// app/actions.js
'use server'

// Это действие **используется** в нашем приложении, поэтому Next.js
// создаст защищённый идентификатор, чтобы клиент мог ссылаться
// и вызывать серверное действие.
export async function updateUserAction(formData) {}

// Это действие **не используется** в нашем приложении, поэтому Next.js
// автоматически удалит этот код во время `next build`
// и не создаст публичную конечную точку.
export async function deleteUserAction(formData) {}

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

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

app/actions.ts
'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('You must be signed in to perform this action')
  }

  // ...
}

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

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

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

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }

  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}
export default async function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }

  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}

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

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

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

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

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

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

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

Полезно знать: Приложения 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.

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

Для получения дополнительной информации ознакомьтесь с документацией React: