0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · Lektion

Feldbezogene Validierungsfehler mit useActionState

Geben Sie strukturierte Validierungsergebnisse aus Actions zurück und zeigen Sie Inline-Fehlermeldungen pro Feld an.

Feldbezogene Validierungsfehler mit useActionState ist eine kostenlose Next.js 15 Fullstack (App Router + Server Actions)-Lektion auf CoddyKit. Dies ist Lektion 3 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des Next.js 15 Fullstack (App Router + Server Actions)-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der Next.js 15 Fullstack (App Router + Server Actions)-Kurs umfasst insgesamt 4 Lektionen.

Teile dieser Lektion wurden noch nicht übersetzt und werden auf Englisch angezeigt.

Why Field-Level Errors?

When a form fails validation, users need to know exactly which field is wrong and why. A single banner saying "Something went wrong" forces them to guess.

In Next.js 15, Server Actions paired with useActionState let you return a structured result from the server and render an inline error right beneath each input.

  • email is invalid → show the error under the email field
  • password is too short → show it under the password field

This lesson shows how to return that structure and render it cleanly.

Shaping the Action State

Start by deciding the shape of what your action returns. A good pattern keeps per-field errors in an errors map keyed by field name, where each value is an array of messages.

Defining a TypeScript type makes the contract explicit for both the action and the component.

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 = {};

Validating with Zod in the Action

A schema library like Zod gives you both validation and a ready-made error structure. Call safeParse so failures don't throw — instead you inspect the result.

flatten().fieldErrors returns exactly the { field: string[] } shape we designed, so it maps directly onto our state.

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

The Server Action Signature

An action used with useActionState receives two arguments: the previous state and the submitted FormData. It must return the next state.

Mark the file or function with 'use server'. Read values with formData.get(...), validate, and return errors instead of throwing.

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

Wiring useActionState

In your client component, call useActionState(action, initialState). It returns a tuple:

  • state — the latest value your action returned
  • formAction — pass this to the form's action prop
  • isPending — true while the action runs (great for disabling the button)

Remember the 'use client' directive — hooks only run in client components.

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

Rendering an Inline Error

To show a field error, read it from state.errors?.fieldName. Because each field holds a string array, render the first message (or map over all of them).

Use optional chaining so the very first render — when errors is undefined — doesn't crash.

<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>

Accessibility: Linking Errors to Inputs

Screen readers should announce the error and tie it to the field. Two attributes do the heavy lifting:

  • aria-invalid — set true when the field has an error
  • aria-describedby — point to the id of the error element

Give the error element a stable id and a polite live region so updates are read aloud.

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

Preserving User Input

Because the form re-renders from server state, an uncontrolled input would lose what the user typed. Return the safe values from the action and feed them back with defaultValue.

Never echo passwords back to the client. Only return non-sensitive fields like email so the user doesn't retype everything after a validation error.

// 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 */}

A Pure Validation Helper You Can Test

Validation logic doesn't need Next.js to be correct. Extract it into a pure function that takes plain values and returns the same { field: string[] } error map. This is trivially unit-testable and runnable on any judge.

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

Multiple Errors per Field

A single field can fail several rules at once (empty, too short, wrong format). Since each entry is an array, you can render every message as a list.

Zod naturally accumulates multiple issues per field, so fieldErrors.password may contain more than one string.

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

Top-Level vs Field Errors

Not every failure belongs to a field. A duplicate-email conflict from the database, or a generic server fault, is a form-level message.

Keep both channels in your state: errors for per-field issues and message for the whole form. Render the top-level message above the fields so it isn't missed.

// 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>}

Quick Check

Test your understanding of the action contract used by useActionState.

Recap

You can now build field-level validation with Server Actions and useActionState:

  • Shape the state with an errors map of { field: string[] }, plus optional values and a top-level message.
  • Validate in the action with safeParse and return flatten().fieldErrors — return, never throw.
  • Wire the hook with useActionState(action, initialState) and pass formAction to the form; use isPending for the button.
  • Render inline from state.errors?.field, with aria-invalid and aria-describedby for accessibility.
  • Preserve input via defaultValue, but never echo passwords.
  • Separate channels: per-field errors vs form-wide message.

Häufig gestellte Fragen

Ist die Lektion „Feldbezogene Validierungsfehler mit useActionState“ kostenlos?

Ja — der vollständige Text von „Feldbezogene Validierungsfehler mit useActionState“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des Next.js 15 Fullstack (App Router + Server Actions)-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der Next.js 15 Fullstack (App Router + Server Actions)-Kurs umfasst insgesamt 4 Lektionen.

Was lerne ich in „Feldbezogene Validierungsfehler mit useActionState“?

Geben Sie strukturierte Validierungsergebnisse aus Actions zurück und zeigen Sie Inline-Fehlermeldungen pro Feld an. Du übst Next.js 15 Fullstack (App Router + Server Actions) mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.

Brauche ich Erfahrung, um Next.js 15 Fullstack (App Router + Server Actions) zu starten?

Keine Vorkenntnisse erforderlich. Next.js 15 Fullstack (App Router + Server Actions) auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 3 von 4.

Wie lange dauert die Lektion „Feldbezogene Validierungsfehler mit useActionState“?

Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.

Kann ich in dieser Next.js 15 Fullstack (App Router + Server Actions)-Lektion Code schreiben und ausführen?

Ja. Jede Next.js 15 Fullstack (App Router + Server Actions)-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.

Alle Lektionen in diesem Kurs

  1. Progressive Enhancement mit der form-Action-Prop
  2. Ausstehende und Ladezustände mit useFormStatus
  3. Feldbezogene Validierungsfehler mit useActionState
  4. Sofortiges Feedback mit useOptimistic
← Zurück zu Next.js 15 Fullstack (App Router + Server Actions)