Формы и мутации данных

Формы позволяют создавать и обновлять данные в веб-приложениях. Next.js предоставляет мощный способ обработки отправки форм и мутаций данных с использованием Server Actions.

Примеры

Как работают Server Actions

С Server Actions вам не нужно вручную создавать API-эндпоинты. Вместо этого вы определяете асинхронные серверные функции, которые можно вызывать напрямую из компонентов.

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

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

Включите Server Actions в файле next.config.js:

next.config.js
module.exports = {
  experimental: {
    serverActions: true,
  },
}

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

  • Формы, вызывающие Server Actions из серверных компонентов, могут работать без JavaScript.
  • Формы, вызывающие Server Actions из клиентских компонентов, будут ставить отправки в очередь, если JavaScript ещё не загружен, отдавая приоритет гидратации клиента.
  • Server Actions наследуют среду выполнения страницы или макета, в котором они используются.
  • Server Actions работают с полностью статическими маршрутами (включая ревалидацию данных с ISR).

Ревалидация кэшированных данных

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

В отличие от традиционных приложений, где на один маршрут приходится одна форма, Server Actions позволяют иметь несколько действий на маршрут. Кроме того, браузеру не нужно обновляться после отправки формы. За один сетевой запрос Next.js может вернуть как обновлённый UI, так и обновлённые данные.

Смотрите примеры ниже для ревалидации данных из Server Actions.

Примеры

Формы только для сервера

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

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

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

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

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

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

Полезно знать: <form action={create}> принимает тип данных FormData. В примере выше FormData, отправленные через HTML form, доступны в серверном действии create.

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

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

'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/')
}
'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/')
}

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

'use server'

import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts')
}
'use server'

import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts')
}

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

Если вы хотите перенаправить пользователя на другой маршрут после завершения Server Action, вы можете использовать redirect и любой абсолютный или относительный URL:

'use server'

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

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Обновить кэшированные посты
  redirect(`/post/${id}`) // Перейти на новый маршрут
}
'use server'

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

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Обновить кэшированные посты
  redirect(`/post/${id}`) // Перейти на новый маршрут
}

Валидация форм

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

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

import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function submit(formData: FormData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}
import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function submit(formData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}

Отображение состояния загрузки

Используйте хук useFormStatus для отображения состояния загрузки при отправке формы на сервер. Хук useFormStatus можно использовать только как дочерний элемент элемента form, использующего Server Action.

Например, следующая кнопка отправки:

'use client'

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

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

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

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

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

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

<SubmitButton /> можно использовать в форме с Server Action:

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

export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}
import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

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

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

'use server'

export async function createTodo(prevState: any, formData: FormData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}
'use server'

export async function createTodo(prevState, formData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}

Затем в клиентском компоненте вы можете прочитать это значение и отобразить сообщение об ошибке.

'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

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

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

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">Введите задачу</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}
'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

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

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

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">Введите задачу</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}

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

Используйте useOptimistic для оптимистичного обновления интерфейса до завершения Server Action, вместо ожидания ответа:

'use client'

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

type Message = {
  message: string
}

export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
    messages,
    (state: Message[], newMessage: string) => [
      ...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 { experimental_useOptimistic as 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>
  )
}

Установка куки

Вы можете установить куки внутри Server Action, используя функцию cookies:

'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}
'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}

Чтение куки

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

'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}
'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}

Удаление куки

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

'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}
'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}

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