Formulare cu Server Actions
Fluxul complet, valori păstrate după eroare, și RHF vs Conform vs TanStack Form.
Actualizat
Ce înseamnă un formular cu Server Action
În Next.js, cel mai direct mod de a trimite un formular e o Server Action: formularul trimite FormData direct la o funcție de pe server, fără API route și fără fetch scris de mână. Lecția asta pune cap la cap tot ce ai învățat: formular → validare Zod pe server → erori înapoi în UI → invalidare de cache.
Fluxul complet
Dacă datele sunt invalide, acțiunea returnează erorile, iar formularul le afișează:
formFormDataaction→Server ActionsafeParse cu Zodinvalid→UIerori per câmp + valorile păstrateDacă sunt valide, acțiunea salvează și reîmprospătează datele:
Server Actiondate validesalvează→DBupdateTag / redirect→UIdate proaspeteImplementarea
// 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: ['Există deja un cont cu acest 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}>Creează cont</button>
</form>
)
}Detalii care contează:
| Detaliu | De ce |
|---|---|
returnezi values și le pui în defaultValue |
după o eroare, React resetează formularul — fără asta userul pierde ce a scris |
| erori de business (email existent) în același format | UI-ul le afișează la fel ca pe cele de validare |
redirect / updateTag doar la succes |
read-your-own-writes — vezi Cache |
| auth + autorizare la începutul acțiunii | acțiunea e un endpoint public |
Validare și pe client — opțiunile
Fluxul de mai sus validează doar pe server: corect și sigur, dar userul vede erorile abia după trimitere. Pentru feedback instant:
| Abordare | Cum | Plusuri | Minusuri |
|---|---|---|---|
doar useActionState |
ca mai sus, + atribute HTML (required, type) |
simplu, merge fără JS | erori abia la submit |
| React Hook Form + Server Action | RHF validează cu zodResolver, apoi apelezi acțiunea cu datele |
UX excelent, ecosistem mare | două modele mentale, nu merge fără JS |
| Conform | construit special pentru Server Actions: aceeași schemă, validare pe client și server, merge fără JS | progressive enhancement real | comunitate mai mică |
| TanStack Form | librărie nouă, complet tipată, agnostică de framework | tipuri foarte bune, performant | mai verbos, mai nou |
| Formik | librăria „clasică” | mult cod vechi o folosește | întreținută slab — nu pentru proiecte noi |
Cum alegi
- Formular simplu (login, contact, newsletter) →
useActionState+ Zod pe server. - Formular complex (multe câmpuri, liste dinamice, validare live) → React Hook Form + Zod, cu aceeași schemă în Server Action.
- Vrei progressive enhancement complet și validare pe ambele părți → Conform.
Indiferent de alegere: schema Zod e una singură, iar serverul validează mereu.
Pe scurt
- Form → Server Action →
safeParse→ erori + valori înapoi, sau salvare +updateTag/redirect. - Păstrează valorile după eroare; erorile de business în același format.
- Simplu →
useActionState; complex → React Hook Form; progressive enhancement → Conform.