Next.js Server Actions űrlapok 2026: useActionState, Zod validáció és progresszív enhancement
Teljes útmutató a Next.js Server Actions, useActionState és Zod validáció használatához 2026-ban. Regisztrációs űrlap példa, progresszív enhancement, hibakezelés és biztonsági ellenőrzés.
A Next.js Server Actions űrlapok 2026-ban a useActionState hook, a Zod validáció és a progresszív enhancement hármasára épülnek: a 'use server' direktívával megjelölt függvény fogadja a FormData objektumot, szerveroldalon validálja Zod sémával, és tipizált eredményt ad vissza, amelyet a useActionState a kliensoldalon megjelenít. Mindezt JavaScript nélkül is működő űrlapban. Ez a megközelítés feleslegessé teszi a külön /api végpontokat, és csökkenti a kliens-állapot kezelését. A cikkben bemutatom a Next.js 16-ban stabil mintát, a tipikus buktatókat, és a produkciós kódra szabott bevált gyakorlatokat (azokat, amelyeket az utolsó pár projektemben magam is használtam).
A useActionState a React 19-ben váltotta fel a korábbi useFormState hookot, és immár a 'react' csomagból importáljuk, nem a 'react-dom'-ból.
A Server Actions Next.js 14 óta stabil, Next.js 16-ban pedig Turbopack-alapú buildelés és per-build kulccsal titkosított action ID-k védik.
Mindig schema.safeParse()-t használj a szerveroldali validációhoz, és error.flatten().fieldErrors formában add vissza a hibákat.
A progresszív enhancement garantálja, hogy az űrlap JavaScript nélkül is beküldhető, ezért kerüld az onSubmit handlereket, és építsd az action propra a logikát.
Külön SubmitButton komponens és useFormStatus kell a pending állapothoz, mert a hook csak <form> gyerekeként működik.
Az autentikációt és a jogosultságot a Server Actionön belül kell ellenőrizni; a kliensoldali ellenőrzés nem elegendő.
Mi az a Server Action a Next.js-ben?
A Server Action egy aszinkron függvény, amely a szerveren fut, és a 'use server' direktívával jelöljük meg. A Next.js App Router automatikusan generál hozzá egy POST végpontot, és titkosítja az action ID-t, így a kliens csak egy átláthatatlan tokent lát. Az ilyen függvényeket közvetlenül átadhatjuk egy <form action={...}> propnak, vagy meghívhatjuk őket button onClick-ből. A hálózati réteget a keretrendszer kezeli.
A Next.js 14 óta a Server Actions stabil API, és a 2026-os Next.js 16 kiadás új biztonsági réteget hozott: minden buildhez tartozik egy egyedi titkosító kulcs, amely megakadályozza az action ID-k kívülről történő találgatását. A hivatalos Next.js űrlapkezelési dokumentáció részletes referenciát ad. A Server Actions tipikus felhasználási területei a mutációk: űrlap-beküldés, gomb-kattintásra történő adatváltoztatás, optimista frissítés. Ha viszont egy külső kliensnek (mobilapp, webhook) is szüksége van az endpointra, akkor inkább a Next.js Route Handlers REST API megközelítés a megfelelőbb választás.
A Server Action és a Route Handler tehát nem versenyez egymással, eltérő esetekre valók. A belső mutációknál a Server Action egyszerűbb, típusbiztos, és alapból progresszív enhancementtel jön. Egy regisztrációs folyamatból kihúz minden felesleges fetch() és useState hívást, amelyet egy hagyományos SPA megkövetelne.
useActionState vs useFormState: mi változott?
A React 19 átnevezte a korábbi useFormState hookot useActionState-re, és áthelyezte a 'react-dom' csomagból a 'react'-be. A funkcionalitás nagyrészt változatlan: a hook egy szerveroldali action-t és egy kezdeti állapotot vár paraméterként, és visszaad egy tuple-t: [state, formAction, isPending]. Az újdonság az isPending flag, amely automatikusan jelzi, hogy az action éppen fut-e (korábban ezt csak a useFormStatus hookkal lehetett figyelni).
A migráció Next.js 14-ről 15-re vagy 16-ra általában az import sor cseréjével kezdődik:
// Régi (Next.js 14, React 18)
import { useFormState } from 'react-dom';
const [state, formAction] = useFormState(action, initialState);
// Új (Next.js 15+, React 19)
import { useActionState } from 'react';
const [state, formAction, isPending] = useActionState(action, initialState);
A React csapat a useActionState hivatalos referenciájában részletezi a teljes API-t. A gyakorlatban az isPending flag azokban a komponensekben hasznos, amelyek nem közvetlenül a <form>-on belül vannak. Például egy oldalsávi summary panel, amely vissza akarja jelezni a folyamatban lévő mutációt. A submit gombhoz továbbra is a useFormStatus hook a tisztább megoldás, mert nem kell prop-drillinggel átadni az állapotot, és a hook automatikusan a legközelebbi szülő <form>-hoz csatlakozik.
Zod séma: a típusbiztos szerződés
A Zod 4 (2026-os stabil verzió) sokkal gyorsabb és kompaktabb, mint a 3.x ág, és bevezette a z.email(), z.url(), z.uuid() top-level függvényeket a z.string().email() formulák helyett. A séma egy külön fájlban él (jellemzően schemas/user.ts), és ugyanazt a sémát használjuk a kliensoldali űrlapvalidációhoz (React Hook Form + @hookform/resolvers/zod) és a szerveroldali Server Actionhöz is. Ezzel megszűnik a duplikáció, és a TypeScript végig egységes típusokkal dolgozik.
// schemas/register.ts
import { z } from 'zod';
export const RegisterSchema = z.object({
email: z.email({ message: 'Érvényes e-mail címet adj meg.' }),
password: z.string()
.min(12, { message: 'A jelszó legalább 12 karakter legyen.' })
.regex(/[A-Z]/, { message: 'Tartalmazzon nagybetűt.' })
.regex(/[0-9]/, { message: 'Tartalmazzon számot.' }),
acceptTerms: z.literal('on', { message: 'El kell fogadnod a feltételeket.' }),
});
export type RegisterInput = z.infer<typeof RegisterSchema>;
Fontos szabály: szerveroldalon mindig safeParse()-t használj, ne parse()-t. A parse() hibadobással jelez, ami megszakítja a Server Action végrehajtását, és a useActionState nem tudja megjeleníteni a hibákat (a felhasználó csak egy generikus error boundary-t fog látni). A safeParse() ezzel szemben egy diszkriminált union-t ad vissza ({ success: true, data } vagy { success: false, error }), és lehetőséget biztosít a kontrollált, mezőszintű hiba-visszaadásra. A flatten().fieldErrors hívás egy Record<string, string[]> formátumba alakítja a hibákat, amely közvetlenül a JSX-ben renderelhető.
Teljes példa: regisztrációs űrlap
Vegyünk egy regisztrációs folyamatot, ahol e-mailt, jelszót és egy szerződéselfogadó checkboxot kérünk. Az adatokat a Drizzle ORM és Next.js adatbázis-integráció mintáját követve PostgreSQL-be írjuk. Először a Server Action:
// app/(auth)/register/actions.ts
'use server';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { db } from '@/db';
import { users } from '@/db/schema';
import { hash } from 'argon2';
import { RegisterSchema } from '@/schemas/register';
export type RegisterState = {
ok: boolean;
fieldErrors?: Record<string, string[]>;
formError?: string;
};
export async function registerAction(
_prev: RegisterState,
formData: FormData,
): Promise<RegisterState> {
const parsed = RegisterSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
acceptTerms: formData.get('acceptTerms'),
});
if (!parsed.success) {
return {
ok: false,
fieldErrors: parsed.error.flatten().fieldErrors,
};
}
const existing = await db.query.users.findFirst({
where: (u, { eq }) => eq(u.email, parsed.data.email),
});
if (existing) {
return { ok: false, formError: 'Ez az e-mail már regisztrálva van.' };
}
const passwordHash = await hash(parsed.data.password);
await db.insert(users).values({
email: parsed.data.email,
passwordHash,
});
revalidatePath('/dashboard');
redirect('/dashboard');
}
Figyeld meg: az action={formAction} propra építünk, nem onSubmit-re. Ez kulcsfontosságú a progresszív enhancementhez, és emiatt a böngésző akkor sem refreshel teljes oldalt, ha még nem futott le a hidratáció.
Hogyan működik a progresszív enhancement?
A progresszív enhancement az a tervezési elv, amely szerint az alapvető funkcionalitás JavaScript nélkül is működik, a JavaScript pedig csak finomítja az élményt. A Server Actions ezt a Next.js-ben natívan támogatja: amikor a felhasználó beküldi az űrlapot egy lassú vagy JS-mentes környezetben (pl. flaky 3G-n, screen readerrel, vagy egy enterprise proxy mögött), a böngésző egy szabványos multipart/form-data POST kérést küld, amelyet a Next.js szervere fogad, lefuttatja az action-t, és újrarendereli az oldalt. JavaScript bekapcsolt állapotában ugyanaz a kód már nem teljes oldalt frissít, hanem React-szinten sodorja be az új state-et.
Őszintén szólva, ennek a működésnek van egy gyakorlati feltétele: az űrlap kliensoldali komponensében ne használj preventDefault()-tot, és ne építs feltételes renderingre, amely csak useEffect után jelenik meg. Ha az űrlap maga is csak hidratáció után jelenik meg, akkor a progresszív enhancement előnye elveszik. Egy jó minta, hogy a RegisterForm komponens egy server component oldalon belül van, és csak a 'use client' határai jelölik ki a hidratációt igénylő részeket.
A React DOM form komponens referenciája részletesen leírja, hogyan kezeli a React a függvényreferenciaként átadott action propot, és milyen rejtett mezőket inject a kliens-szerver action ID egyeztetéshez.
Hibakezelés és pending állapot
A Server Actionökben két jellegű hiba létezik: validációs hibák (a felhasználó adta meg rosszul) és váratlan hibák (DB outage, harmadik fél API nem válaszol). A kettőt eltérően kell kezelni. A validációs hibákat visszaadni kell a state objektumon keresztül, soha ne dobd őket. A váratlan hibákat hagyd, hogy a Next.js error boundary kapja el (error.tsx), és csak akkor csomagold try/catch-be, ha specifikus felhasználói üzenetet akarsz visszaadni.
try {
await db.insert(users).values({ ... });
} catch (err) {
// Csak a várt hibákat fordítsd state-té
if (err instanceof PostgresError && err.code === '23505') {
return { ok: false, formError: 'Ez a felhasználó már létezik.' };
}
throw err; // a többit hagyd a Next.js error boundary-re
}
A pending állapotot kétféleképpen érheted el: az useActionState harmadik visszatérési értékével (a teljes komponensen belül), vagy a useFormStatus hookkal (csak <form> gyerekkomponensben). A két API nem cseréli ki egymást. A useFormStatus egy szűkebb, lokalizáltabb hook, és azért hasznos, mert a submit button komponens nem kapja meg propként a pending státuszt. Ha próbálnád átadni propként, akkor a SubmitButton minden state változásnál újrarenderelődne, ami felesleges.
Autentikáció és jogosultság ellenőrzése
A Server Actions egy gyakori, veszélyes félreértése, hogy „csak hitelesített felhasználók hívják meg, mert a UI is rejtve van". Ez nem igaz: az action ID-t valaki bármikor küldheti egy direkt POST kéréssel. Egy korábbi projektemen pont ebbe a hibába futottam: egy admin-only delete gomb mögött nem volt szerveroldali ellenőrzés, és egy egyszerű cURL-lel reprodukálható volt a támadás. Azóta minden mutáló action elejére odakerül a session-ellenőrzés.
Az autentikáció részleteit lefedi az Auth.js v5 hitelesítés Next.js App Routerben bemutatóm. A fontos elv: a kliensoldali UI rejtés sosem helyettesíti a szerveroldali ellenőrzést. Ha egy gomb csak admin felhasználóknak látszik, az még nem védi az action-t. Szerveren is ki kell zárni a nem-admin hívókat. Egy gyakorlati minta, hogy az action elején van egy requireUser() vagy requireRole('admin') helper, amely vagy session-t ad vissza, vagy dob egy UnauthorizedError-t.
Optimista UI useOptimistic-kal
A React 19 useOptimistic hookja lehetővé teszi, hogy a felhasználó azonnal lássa a változás eredményét, miközben a Server Action a háttérben fut. Ha a szerver hibát ad, a hook automatikusan visszaállítja a korábbi állapotot. Ez különösen jó „kedvelés" gombokhoz, listához hozzáadáshoz, vagy state-toggle gombokhoz.
A példában a useOptimistic a kezdeti állapotból generálja az optimistic verziót, majd amikor az action befejeződik és a parent komponens újrarenderel, a hook automatikusan az új, valódi state-re vált. Ha a Server Action hibát dob, a React visszaesik az utolsó megerősített állapotra. Fontos, hogy a parent komponens a frissítés után revalidatePath hatására új propokat kapjon, egyébként az optimistic state egyszerűen elveszik a következő renderben.
Tipikus buktatók és hibák
Az alábbi hibák a legtöbb produkciós kódbázisban előfordulnak. Érdemes kódellenőrzéskor figyelni rájuk:
Hiányzó 'use server' direktíva: ha a fájl tetejére elfelejted kiírni, a Next.js a klienskódba bundle-eli a függvényt, és titkos adatok (DB connection string, API kulcsok) szivároghatnak ki.
Kliensoldali onSubmit használata az action helyett: elveszik a progresszív enhancement, és kell egy külön loading state.
Generikus Error dobása validációkor: a useActionState nem fogja megjeleníteni a hibát; a felhasználó csak egy generikus error.tsx oldalt lát.
Nem hívod a revalidatePath-t mutáció után: a régi adat marad a cache-ben, és a felhasználó nem látja a változást, amíg manuálisan nem frissít. Ez különösen érzékelhető Next.js 16 Cache Components és Partial Prerendering használatakor.
Túl nagy file feltöltése FormData-ban: a Server Actions alapértelmezett body limit 1 MB. Nagyobb fájlokhoz a next.config.ts-ben állítsd át a serverActions.bodySizeLimit-et, vagy használj presigned S3 URL-t.
Race condition optimista UI-nál: ha a felhasználó gyorsan kattint kétszer, két párhuzamos action futhat. Használj useTransition-t és tiltsd a gombot pending alatt.
A Zod hivatalos dokumentációja rengeteg további mintát ad a sémák kompozíciójához, transzformációkhoz, és async validációhoz (pl. e-mail egyediség ellenőrzése). Komplexebb űrlapoknál érdemes a sémát több, kisebb darabra törni, és a .merge() vagy .extend() módszerekkel kombinálni.
Gyakran ismételt kérdések
Mi a különbség a useFormState és a useActionState között?
A useActionState a React 19-ben bevezetett új neve a useFormState hooknak, és immár a 'react' csomagból importálandó, nem a 'react-dom'-ból. Az API gyakorlatilag azonos, de a useActionState harmadik elemként visszaadja az isPending flaget is, így nincs szükség külön useFormStatus hookra a pending állapot követéséhez a form-on kívül.
Helyettesíthetik a Server Actions a REST API-kat?
Részben igen, részben nem. A Next.js alkalmazáson belüli mutációkhoz (űrlapok, gomb-kattintásra történő frissítés) a Server Actions egyszerűbb, típusbiztos és progresszív enhancementtel jön. Külső kliensek (mobilalkalmazás, harmadik fél webhookja, publikus API) számára viszont továbbra is Route Handlerek kellenek, mert a Server Action ID-k titkosítva és per-build változnak.
Hogyan tudok fájlt feltölteni Server Actionnel?
A FormData.get('file') hívás File objektumot ad vissza, amelyet a Server Actionön belül feldolgozhatsz. Alapértelmezetten 1 MB a body méretkorlát; nagyobb fájlokhoz állítsd át a serverActions.bodySizeLimit-et a next.config.ts-ben, vagy generálj presigned S3 URL-t a kliensnek és töltsd közvetlenül oda.
Miért nem jelennek meg a Zod validációs hibáim a felhasználónak?
A leggyakoribb ok, hogy schema.parse()-t használsz safeParse() helyett, és ezért a Zod kivételt dob, ami a Next.js error boundary-jét triggereli. A megoldás: használj safeParse()-t, ellenőrizd a success mezőt, és add vissza a hibákat error.flatten().fieldErrors formában a useActionState state-jén keresztül.
Biztonságosak a Server Actions CSRF támadás ellen?
Igen, a Next.js automatikusan ellenőrzi az Origin és Host header egyezést a Server Action hívásoknál, és a Next.js 16-tól minden buildhez egyedi titkosított action ID tartozik. Ennek ellenére mindig ellenőrizd a szerveroldalon a felhasználó autentikációját és jogosultságát; a kliensoldali UI elrejtés önmagában nem biztonsági réteg.