Spec-Zone.ru › Next.js

Действия и мутации на сервере

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

🎥 Смотреть: Узнайте больше о формах и мутациях с помощью действий на сервере → YouTube (10 минут).

Конвенция

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

Компоненты сервера

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

// Server Component
export default function Page() {
  // Server Action
  async function create() {
    'use server'
 
    // ...
  }
 
  return (
    // ...
  )
}

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

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

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

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

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

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

Поведение

  • Действия на сервере могут быть вызваны с помощью атрибута action в элементе <form>:
    • Компоненты сервера по умолчанию поддерживают поэтапное улучшение, что означает, что форма будет отправлена, даже если JavaScript еще не загружен или отключен.
    • В компонентах клиента формы, вызывающие действия на сервере, будут приостанавливать отправку, если JavaScript еще не загружен, отдавая приоритет загрузке клиента.
    • После загрузки браузер не перезагружается при отправке формы.
  • Действия на сервере не ограничиваются <form> и могут быть вызваны из обработчиков событий, useEffect, сторонних библиотек и других элементов формы, таких как <button>.
  • Действия на сервере интегрируются с архитектурой кэширования и перепроверки Next.js. Когда действие вызывается, Next.js может вернуть обновленный интерфейс пользователя и новые данные в одном запросе на сервер.
  • Внутри действий используется метод 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'),
    }
 
    // mutate data
    // revalidate cache
  }
 
  return <form action={createInvoice}>...</form>
}

Важно знать:

  • Пример: Форма с состояниями загрузки и ошибок
  • При работе с формами, содержащими много полей, вам может потребоваться использовать метод entries() с JavaScript's Object.fromEntries(). Например: const rawFormData = Object.fromEntries(formData). Следует отметить, что formData будет включать дополнительные $ACTION_ свойства.
  • См. Документацию React <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">Update User Name</button>
    </form>
  )
}

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

'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}>
      Add
    </button>
  )
}

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

import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
 
// Server Component
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: 'Invalid Email',
  }),
})
 
export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
 
  // Return early if the form data is invalid
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // Mutate data
}

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

  • Передав действие useFormState, сигнатура функции действия изменится, чтобы принять новый параметр prevState или initialState в качестве первого аргумента.
  • useFormState — это хук React и, следовательно, должен использоваться в компоненте клиента.
'use server'
 
export async function createUser(prevState: any, formData: FormData) {
  // ...
  return {
    message: 'Please enter a valid 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>Sign up</button>
    </form>
  )
}

Важно знать:

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

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

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

'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">Send</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>
  )
}
END_OF_DOCUMENT_MARKER

Это запустит отправку ближайшего предка <form>, который вызовет действие сервера.

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

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

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

Вы можете вызывать действие сервера из обработчиков событий, таких как 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>
    </>
  )
}

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

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

'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 } 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>Total Views: {views}</p>
}

Помните о поведении и нюансах useEffect.

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

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

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

'use server'
 
export async function createTodo(prevState: any, formData: FormData) {
  try {
    // Mutate data
  } catch (e) {
    throw new Error('Failed to create task')
  }
}

Важно знать:

  • Помимо выброса ошибки, вы также можете вернуть объект, который будет обработан useFormState. См. Валидацию на стороне сервера и обработку ошибок.

Перепроверка данных

Вы можете перепроверить кэш Next.js в своих действиях сервера с помощью API revalidatePath:

'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')
}

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

Если вы хотите перенаправить пользователя на другой маршрут после завершения действия сервера, вы можете использовать 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') // Update cached posts
  redirect(`/post/${id}`) // Navigate to the new post page
}

Куки

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

'use server'
 
import { cookies } from 'next/headers'
 
export async function exampleAction() {
  // Get cookie
  const value = cookies().get('name')?.value
 
  // Set cookie
  cookies().set('name', 'Delba')
 
  // Delete cookie
  cookies().delete('name')
}

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

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

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

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

'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')
  }
 
  // ...
}

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

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

export default function Page() {
  const publishVersion = await getLatestVersion();
 
  async function publish(formData: FormData) {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }
 
  return <button action={publish}>Publish</button>;
}

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

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

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

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

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

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

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

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

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

Поскольку действия сервера могут быть вызваны в элементе <form>, это открывает их для атак CSRF.

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

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

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

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

Узнайте больше о безопасности и действиях сервера.

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

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

  • "use server"
  • <form>
  • useFormStatus
  • useFormState
  • useOptimistic

© 2024 Vercel, Inc.
Licensed under the MIT License.
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API