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.

Next.js Server Actions Útmutató (2026)

Frissítve: 2026. június 4.

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');
}

Most a kliens-oldali űrlap:

// app/(auth)/register/form.tsx
'use client';

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { registerAction, type RegisterState } from './actions';

const initialState: RegisterState = { ok: false };

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending} aria-disabled={pending}>
      {pending ? 'Regisztráció folyamatban…' : 'Regisztrálok'}
    </button>
  );
}

export function RegisterForm() {
  const [state, formAction] = useActionState(registerAction, initialState);

  return (
    <form action={formAction} noValidate>
      <label htmlFor="email">E-mail</label>
      <input id="email" name="email" type="email" required />
      {state.fieldErrors?.email?.map((msg) => (
        <p key={msg} role="alert">{msg}</p>
      ))}

      <label htmlFor="password">Jelszó</label>
      <input id="password" name="password" type="password" required />
      {state.fieldErrors?.password?.map((msg) => (
        <p key={msg} role="alert">{msg}</p>
      ))}

      <label>
        <input type="checkbox" name="acceptTerms" /> Elfogadom az ÁSZF-et
      </label>
      {state.fieldErrors?.acceptTerms?.map((msg) => (
        <p key={msg} role="alert">{msg}</p>
      ))}

      {state.formError && <p role="alert">{state.formError}</p>}

      <SubmitButton />
    </form>
  );
}

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.

'use server';
import { auth } from '@/auth';

export async function deletePost(_prev: State, formData: FormData) {
  const session = await auth();
  if (!session?.user) {
    return { ok: false, formError: 'Be kell jelentkezned.' };
  }

  const postId = String(formData.get('postId'));
  const post = await db.query.posts.findFirst({
    where: (p, { eq }) => eq(p.id, postId),
  });
  if (!post || post.authorId !== session.user.id) {
    return { ok: false, formError: 'Nincs jogosultságod.' };
  }

  await db.delete(posts).where(eq(posts.id, postId));
  revalidatePath('/posts');
  return { ok: true };
}

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.

'use client';
import { useOptimistic } from 'react';
import { toggleLikeAction } from './actions';

export function LikeButton({ postId, liked, count }: Props) {
  const [optimistic, addOptimistic] = useOptimistic(
    { liked, count },
    (state) => ({
      liked: !state.liked,
      count: state.count + (state.liked ? -1 : 1),
    }),
  );

  return (
    <form action={async () => {
      addOptimistic(null);
      await toggleLikeAction(postId);
    }}>
      <button>{optimistic.liked ? '♥' : '♡'} {optimistic.count}</button>
    </form>
  );
}

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.

Editorial Team
A Szerzőről Editorial Team

Our team of expert writers and editors.