Создание форм с использованием Server Actions
React Server Actions — это серверные функции (Server Functions), которые выполняются на сервере. Их можно вызывать в серверных и клиентских компонентах для обработки отправки форм. В этом руководстве вы узнаете, как создавать формы в Next.js с использованием Server Actions.
Как это работает
React расширяет HTML-элемент <form>
, позволяя вызывать Server Actions с помощью атрибута action
.
При использовании в форме функция автоматически получает объект FormData
. Затем вы можете извлечь данные с помощью методов 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>
}
Полезно знать: При работе с формами, содержащими несколько полей, можно использовать метод
entries()
вместе с JavaScript-функциейObject.fromEntries()
. Например:const rawFormData = Object.fromEntries(formData)
.
Передача дополнительных аргументов
Помимо полей формы, вы можете передавать дополнительные аргументы в серверную функцию с помощью JavaScript-метода bind
. Например, чтобы передать аргумент userId
в серверную функцию updateUser
:
'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
в качестве дополнительного аргумента:
'use server'
export async function updateUser(userId: string, formData: FormData) {}
'use server'
export async function updateUser(userId, formData) {}
Полезно знать:
- Альтернативный вариант — передавать аргументы как скрытые поля формы (например,
<input type="hidden" name="userId" value={userId} />
). Однако значение будет частью отображаемого HTML и не будет закодировано.- Метод
bind
работает как в серверных, так и в клиентских компонентах и поддерживает прогрессивное улучшение.
Валидация форм
Формы можно валидировать как на клиенте, так и на сервере.
- Для клиентской валидации можно использовать 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,
}
}
// Изменяем данные
}
Ошибки валидации
Для отображения ошибок или сообщений валидации преобразуйте компонент, определяющий <form>
, в клиентский компонент и используйте React-хук useActionState
.
При использовании useActionState
сигнатура серверной функции изменится: первым аргументом будет параметр prevState
или initialState
.
'use server'
import { z } from 'zod'
export async function createUser(initialState: any, formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}
'use server'
import { z } from 'zod'
// ...
export async function createUser(initialState, formData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}
Затем вы можете условно отображать сообщение об ошибке на основе объекта state
.
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>Зарегистрироваться</button>
</form>
)
}
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>Зарегистрироваться</button>
</form>
)
}
Состояния ожидания
Хук useActionState
предоставляет булево значение pending
, которое можно использовать для отображения индикатора загрузки или отключения кнопки отправки во время выполнения действия.
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
{/* Другие элементы формы */}
<button disabled={pending}>Зарегистрироваться</button>
</form>
)
}
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
{/* Другие элементы формы */}
<button disabled={pending}>Зарегистрироваться</button>
</form>
)
}
Альтернативно можно использовать хук useFormStatus
для отображения индикатора загрузки во время выполнения действия. При использовании этого хука потребуется создать отдельный компонент для отображения индикатора. Например, чтобы отключить кнопку, пока действие выполняется:
Затем можно вложить компонент SubmitButton
внутрь формы:
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* Другие элементы формы */}
<SubmitButton />
</form>
)
}
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* Другие элементы формы */}
<SubmitButton />
</form>
)
}
Полезно знать: В React 19 хук
useFormStatus
включает дополнительные ключи в возвращаемом объекте, такие как data, method и action. Если вы не используете React 19, доступен только ключpending
.
Оптимистичные обновления
Можно использовать 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 }])
const formAction = async (formData: FormData) => {
const message = formData.get('message') as string
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<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 }]
)
const formAction = async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Отправить</button>
</form>
</div>
)
}
Вложенные элементы форм
Вы можете вызывать Server Actions в элементах, вложенных в <form>
, таких как <button>
, <input type="submit">
и <input type="image">
. Эти элементы принимают проп formAction
или обработчики событий.
Это полезно в случаях, когда нужно вызвать несколько Server Actions в одной форме. Например, можно создать отдельный элемент <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>
, что приведёт к выполнению серверной функции.