Формы с Server Actions
Полный сценарий, сохранение значений после ошибки и RHF против Conform против TanStack Form.
Обновлено
Что значит форма с Server Action
В Next.js самый прямой способ отправить форму — Server Action: форма отправляет FormData прямо в функцию на сервере, без API-маршрута и без написанного вручную fetch. Этот урок собирает вместе всё изученное: форма → валидация Zod на сервере → ошибки обратно в интерфейс → инвалидация кэша.
Полный сценарий
Если данные неверны, действие возвращает ошибки, и форма их показывает:
формаFormDataaction→Server ActionsafeParse через Zodневерно→UIошибки по полям + сохранённые значенияЕсли верны, действие сохраняет и обновляет данные:
Server Actionверные данныесохраняет→БДupdateTag / redirect→UIсвежие данныеРеализация
// features/signup/api/signup-action.ts
'use server'
import { z } from 'zod'
import { redirect } from 'next/navigation'
import { signupSchema } from '../model/signup-schema'
export type SignupState = {
errors?: Partial<Record<keyof z.infer<typeof signupSchema>, string[]>>
values?: Record<string, string>
message?: string
}
export async function signup(prev: SignupState, formData: FormData): Promise<SignupState> {
const raw = Object.fromEntries(formData) as Record<string, string>
const result = signupSchema.safeParse(raw)
if (!result.success) return { errors: z.flattenError(result.error).fieldErrors, values: raw }
const exists = await db.user.findUnique({ where: { email: result.data.email } })
if (exists) return { errors: { email: ['Аккаунт с таким email уже есть'] }, values: raw }
await db.user.create({ data: result.data })
redirect('/welcome')
}// features/signup/ui/signup-form.tsx
'use client'
import { useActionState } from 'react'
import { signup } from '../api/signup-action'
export function SignupForm() {
const [state, action, pending] = useActionState(signup, {})
return (
<form action={action}>
<label htmlFor="email">Email</label>
<input id="email" name="email" defaultValue={state.values?.email} aria-invalid={!!state.errors?.email} aria-describedby="email-error" />
<p id="email-error" role="alert">{state.errors?.email?.[0]}</p>
<button disabled={pending}>Создать аккаунт</button>
</form>
)
}Важные детали:
| Деталь | Почему |
|---|---|
возвращаете values и кладёте их в defaultValue |
после ошибки React сбрасывает форму — без этого пользователь теряет введённое |
| бизнес-ошибки (email уже есть) в том же формате | интерфейс показывает их так же, как ошибки валидации |
redirect / updateTag только при успехе |
read-your-own-writes — см. Кэш |
| аутентификация + авторизация в начале действия | действие — публичный эндпоинт |
Валидация и на клиенте — варианты
Сценарий выше проверяет только на сервере: правильно и безопасно, но ошибки пользователь видит лишь после отправки. Для мгновенной обратной связи:
| Подход | Как | Плюсы | Минусы |
|---|---|---|---|
только useActionState |
как выше, + HTML-атрибуты (required, type) |
просто, работает без JS | ошибки только при отправке |
| React Hook Form + Server Action | RHF проверяет через zodResolver, затем вызываете действие с данными |
отличный UX, большая экосистема | две модели мышления, не работает без JS |
| Conform | создан специально для Server Actions: одна схема, проверка на клиенте и сервере, работает без JS | настоящий progressive enhancement | меньше сообщество |
| TanStack Form | новая, полностью типизированная, не зависящая от фреймворка | очень хорошие типы, быстро | многословнее, новее |
| Formik | «классическая» библиотека | много старого кода на ней | плохо поддерживается — не для новых проектов |
Как выбрать
- Простая форма (вход, контакты, рассылка) →
useActionState+ Zod на сервере. - Сложная форма (много полей, динамические списки, живая валидация) → React Hook Form + Zod, с той же схемой в Server Action.
- Нужен полный progressive enhancement и проверка с обеих сторон → Conform.
Что бы вы ни выбрали: схема Zod одна, а сервер проверяет всегда.
Коротко
- Форма → Server Action →
safeParse→ ошибки + значения обратно или сохранение +updateTag/redirect. - Сохраняйте значения после ошибки; бизнес-ошибки — в том же формате.
- Просто →
useActionState; сложно → React Hook Form; progressive enhancement → Conform.