Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) · Les

Validatiefouten per veld met useActionState

Retourneer gestructureerde validatieresultaten uit actions en geef inline foutmeldingen per veld weer.

Les 3 van 413 stappen

Validatiefouten per veld met useActionState is een gratis Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)-les op CoddyKit. Dit is les 3 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Fullstackontwikkeling met Next.js 15 (App Router + Server Actions). Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat in totaal 4 lessen.

Waarom fouten op veldniveau?

Wanneer een formulier de validatie niet doorstaat, moeten gebruikers precies weten welk veld fout is en waarom. Een enkele banner met de tekst "Er is iets misgegaan" dwingt hen te raden.

In Next.js 15 kun je met Server Actions in combinatie met useActionState een gestructureerd resultaat van de server teruggeven en direct onder elke invoer een foutmelding weergeven.

  • email is ongeldig → toon de fout onder het e-mailveld
  • password is te kort → toon de fout onder het wachtwoordveld

In deze les leer je hoe je die structuur teruggeeft en netjes rendert.

De actiestatus structureren

Bepaal eerst de structuur van wat je actie retourneert. Een goed patroon bewaart fouten per veld in een errors-toewijzing met de veldnaam als sleutel, waarbij elke waarde een array met meldingen is.

Door een TypeScript-type te definiëren, leg je het contract voor zowel de actie als de component expliciet vast.

export type FieldErrors = {
  email?: string[];
  password?: string[];
};

export type SignupState = {
  errors?: FieldErrors;
  // value the user typed, so we can re-fill the form
  values?: { email?: string };
  // a top-level message for non-field errors
  message?: string;
};

export const initialState: SignupState = {};

Valideren met Zod in de actie

Een schemasbibliotheek zoals Zod biedt zowel validatie als een kant-en-klare foutstructuur. Roep safeParse aan zodat fouten niet worden opgegooid — in plaats daarvan controleer je het resultaat.

flatten().fieldErrors retourneert precies de ontworpen vorm { field: string[] }, zodat deze rechtstreeks op onze status kan worden afgebeeld.

import { z } from 'zod';

const SignupSchema = z.object({
  email: z.string().email('Enter a valid email'),
  password: z.string().min(8, 'At least 8 characters'),
});

const result = SignupSchema.safeParse({
  email: 'not-an-email',
  password: '123',
});

if (!result.success) {
  // { email: ['Enter a valid email'], password: ['At least 8 characters'] }
  console.log(result.error.flatten().fieldErrors);
}

De handtekening van de Server Action

Een actie die met useActionState wordt gebruikt, ontvangt twee argumenten: de vorige status en de verzonden FormData. De actie moet de volgende status retourneren.

Markeer het bestand of de functie met 'use server'. Lees waarden met formData.get(...), valideer ze en retourneer fouten in plaats van ze op te gooien.

'use server';

import { z } from 'zod';
import type { SignupState } from './state';

const SignupSchema = z.object({
  email: z.string().email('Enter a valid email'),
  password: z.string().min(8, 'At least 8 characters'),
});

export async function signup(
  prevState: SignupState,
  formData: FormData,
): Promise<SignupState> {
  const email = String(formData.get('email') ?? '');
  const password = String(formData.get('password') ?? '');

  const parsed = SignupSchema.safeParse({ email, password });

  if (!parsed.success) {
    return {
      errors: parsed.error.flatten().fieldErrors,
      values: { email }, // keep email, never echo the password
    };
  }

  // ...persist the user here...
  return { message: 'Account created' };
}

useActionState aansluiten

Roep in je clientcomponent useActionState(action, initialState) aan. Deze hook retourneert een tuple:

  • state — de meest recente waarde die je actie heeft geretourneerd
  • formAction — geef deze door aan de action-prop van het formulier
  • isPending — waar zolang de actie wordt uitgevoerd, handig om de knop uit te schakelen

Vergeet de instructie 'use client' niet — hooks worden alleen in clientcomponenten uitgevoerd.

'use client';

import { useActionState } from 'react';
import { signup } from './actions';
import { initialState } from './state';

export function SignupForm() {
  const [state, formAction, isPending] = useActionState(
    signup,
    initialState,
  );

  return (
    <form action={formAction}>
      {/* inputs go here */}
      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Sign up'}
      </button>
    </form>
  );
}

Een inline foutmelding renderen

Lees een fout voor een veld uit met state.errors?.fieldName. Omdat elk veld een array met tekenreeksen bevat, render je de eerste melding (of je loopt ze allemaal langs).

Gebruik optional chaining zodat de eerste render — wanneer errors undefined is — geen fout veroorzaakt.

<div>
  <label htmlFor="email">Email</label>
  <input
    id="email"
    name="email"
    type="email"
    defaultValue={state.values?.email}
  />
  {state.errors?.email && (
    <p className="error">{state.errors.email[0]}</p>
  )}
</div>

Toegankelijkheid: fouten aan invoervelden koppelen

Schermlezers moeten de foutmelding aankondigen en aan het veld koppelen. Twee attributen doen hierbij het meeste werk:

  • aria-invalid — stel dit in op true wanneer het veld een fout bevat
  • aria-describedby — verwijs naar de id van het foutmeldingselement

Geef het foutmeldingselement een stabiele id en een beleefde live-regio, zodat updates hardop worden voorgelezen.

<input
  id="email"
  name="email"
  type="email"
  defaultValue={state.values?.email}
  aria-invalid={!!state.errors?.email}
  aria-describedby="email-error"
/>
{state.errors?.email && (
  <p id="email-error" className="error" aria-live="polite">
    {state.errors.email[0]}
  </p>
)}

Gebruikersinvoer behouden

Omdat het formulier opnieuw wordt gerenderd vanuit de serverstatus, zou een ongecontroleerde invoer verliezen wat de gebruiker heeft getypt. Retourneer de veilige waarden vanuit de actie en geef ze terug via defaultValue.

Stuur wachtwoorden nooit terug naar de client. Retourneer alleen niet-gevoelige velden, zoals e-mail, zodat de gebruiker na een validatiefout niet alles opnieuw hoeft in te voeren.

// in the action, on failure:
return {
  errors: parsed.error.flatten().fieldErrors,
  values: { email }, // safe to round-trip
  // password is intentionally omitted
};

// in the component:
<input name="email" defaultValue={state.values?.email} />
<input name="password" type="password" /> {/* always blank */}

Een pure validatiehelper die je kunt testen

Voor correcte validatielogica heb je Next.js niet nodig. Haal de logica uit naar een pure functie die gewone waarden ontvangt en dezelfde fouttoewijzing { field: string[] } retourneert. Zo kun je deze eenvoudig met unittests testen en in elke beoordelaar uitvoeren.

type FieldErrors = { email?: string[]; password?: string[] };

function validateSignup(email: string, password: string): FieldErrors {
  const errors: FieldErrors = {};

  const emailOk = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
  if (!emailOk) errors.email = ['Enter a valid email'];

  if (password.length < 8) errors.password = ['At least 8 characters'];

  return errors;
}

const e1 = validateSignup('bad', '123');
console.log(e1); // { email: [...], password: [...] }

const e2 = validateSignup('a@b.co', 'longenough');
console.log(Object.keys(e2).length === 0); // true

console.log(JSON.stringify(validateSignup('x@y.com', 'short')));

Meerdere fouten per veld

Een veld kan meerdere regels tegelijk schenden (leeg, te kort, onjuist formaat). Omdat elke invoer een array is, kun je elke melding als onderdeel van een lijst weergeven.

Zod verzamelt automatisch meerdere problemen per veld, dus fieldErrors.password kan meer dan één tekenreeks bevatten.

{state.errors?.password && (
  <ul className="error-list">
    {state.errors.password.map((msg) => (
      <li key={msg}>{msg}</li>
    ))}
  </ul>
)}

Fouten op formulierniveau versus veldniveau

Niet elke fout hoort bij een veld. Een conflict door een dubbel e-mailadres uit de database of een algemene serverfout is een melding op formulierniveau.

Bewaar beide kanalen in je status: errors voor problemen per veld en message voor het hele formulier. Render de melding op formulierniveau boven de velden, zodat deze niet over het hoofd wordt gezien.

// in the action, after a successful parse:
try {
  await createUser(email, password);
} catch (err) {
  if (isUniqueViolation(err)) {
    return { errors: { email: ['Email already in use'] } };
  }
  return { message: 'Something went wrong. Please try again.' };
}

// in the component, above the fields:
{state.message && <p role="alert">{state.message}</p>}

Korte controle

Toets je begrip van het actiecontract dat door useActionState wordt gebruikt.

Samenvatting

Je kunt nu validatie op veldniveau bouwen met Server Actions en useActionState:

  • Structureer de status met een errors-toewijzing van { field: string[] }, plus optionele values en een message op formulierniveau.
  • Valideer in de actie met safeParse en retourneer flatten().fieldErrors — retourneer de fouten, gooi ze nooit op.
  • Sluit de hook aan met useActionState(action, initialState) en geef formAction door aan het formulier; gebruik isPending voor de knop.
  • Render inline vanuit state.errors?.field, met aria-invalid en aria-describedby voor toegankelijkheid.
  • Behoud de invoer via defaultValue, maar stuur wachtwoorden nooit terug.
  • Scheid de kanalen: errors per veld tegenover message voor het hele formulier.
Gratis beginnen

Leer TypeScript met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
22
Lessen
88

Veelgestelde vragen

Is de les “Validatiefouten per veld met useActionState” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad Fullstackontwikkeling met Next.js 15 (App Router + Server Actions), waaronder “Validatiefouten per veld met useActionState”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat in totaal 4 lessen.

Wat leer ik in “Validatiefouten per veld met useActionState”?

Retourneer gestructureerde validatieresultaten uit actions en geef inline foutmeldingen per veld weer. Je oefent met Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) te beginnen?

Ervaring vooraf is niet nodig. Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.

Hoe lang duurt de les “Validatiefouten per veld met useActionState”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)?

Ja. Elke les over Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Progressive enhancement met de form Action-prop
  2. Pending- en loadingstatussen met useFormStatus
  3. Validatiefouten per veld met useActionState
  4. Directe feedback met useOptimistic
← Terug naar Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)