useActionState: Fehlerbehandlung bei Server Actions strukturiert loesen
AI generated
{ }
React · Server Actions · Formulare
useActionState
strukturierte Fehlerbehandlung bei Server Actions statt verstreutem try/catch

Validierungsfehler pro Feld zurueckgeben, Pending-Zustand mitfuehren und trotzdem deklarativ bleiben: useActionState buendelt das Ergebnis einer Server Action in einem einzigen, vorhersagbaren State statt in verstreuten useState-Aufrufen.

14 Min. Lesezeit useActionState Server Actions Formular-Validierung

1. Was useActionState loest und woher der Name kommt

useActionState hiess in fruehen React-19-Vorabversionen noch useFormState und wurde umbenannt, weil der Hook laengst nicht mehr nur fuer Formulare gedacht ist, sondern fuer jede asynchrone Aktion, deren Ergebnis den UI-Zustand beeinflussen soll. Der Hook nimmt eine Action-Funktion und einen initialen State entgegen und liefert drei Werte zurueck: den aktuellen State, eine an das form-Element oder einen Button anbindbare Wrapper-Action und ein isPending-Flag, das automatisch waehrend der laufenden Aktion true ist.

Der entscheidende Unterschied zu einem manuellen Ansatz mit useState und einem eigenen async Handler ist, dass useActionState den Zustand konsistent an das Ergebnis der letzten abgeschlossenen Aktion bindet, inklusive korrektem Verhalten bei mehreren schnell aufeinanderfolgenden Absendungen, ohne dass man Race Conditions selbst gegen veraltete Antworten absichern muss. React sorgt intern dafuer, dass nur das Ergebnis der zuletzt gestarteten Aktion tatsaechlich im State landet.

2. Grundstruktur: Action-Funktion und Formular verbinden

Die an useActionState uebergebene Funktion bekommt als ersten Parameter den vorherigen State und als zweiten die FormData des abgesendeten Formulars, und muss den neuen State zurueckgeben, der danach als aktueller State aus dem Hook verfuegbar ist. Diese Signatur erlaubt es, den vorherigen Zustand fuer Vergleiche heranzuziehen, etwa um zu erkennen, ob es sich um den ersten Versuch oder einen wiederholten Versuch nach einem Fehler handelt, ohne dass man diesen Zustand separat mitfuehren muesste.

Die vom Hook zurueckgegebene Wrapper-Action wird direkt als action-Attribut an ein form-Element gebunden, wodurch React automatisch die native FormData-Extraktion, das Verhindern des klassischen Seiten-Reloads und die Aktualisierung des isPending-Flags uebernimmt. Wichtig dabei: Die Action-Funktion selbst muss als Server Action markiert sein (mit der 'use server' Direktive am Dateianfang oder Funktionsanfang), wenn sie tatsaechlich serverseitig ausgefuehrt werden soll, useActionState funktioniert aber ebenso mit rein clientseitigen async Funktionen.


'use server';

export async function updateProfile(previousState, formData) {
  const email = formData.get('email');

  if (!email || !email.includes('@')) {
    return {
      success: false,
      errors: { email: 'Bitte eine gueltige E-Mail-Adresse angeben.' },
      values: { email },
    };
  }

  await db.user.update({ where: { id: previousState.userId }, data: { email } });

  return { success: true, errors: {}, values: { email } };
}

3. Validierungsfehler pro Feld strukturiert zurueckgeben

Der zentrale Vorteil von useActionState fuer Formulare mit mehreren Feldern liegt darin, dass der zurueckgegebene State ein beliebig strukturiertes Objekt sein darf, typischerweise mit einem errors-Feld, das pro Formularfeld eine eigene Fehlermeldung enthaelt. Statt eines einzigen globalen Fehlertexts kann jedes Eingabefeld gezielt seine eigene Meldung anzeigen, indem die Komponente auf state.errors.email, state.errors.password und so weiter zugreift, was fuer Nutzer deutlich hilfreicher ist als eine Sammelmeldung am Formularende.

Zusaetzlich zu den Fehlermeldungen lohnt es sich, die zuletzt eingegebenen Werte im selben State-Objekt mitzufuehren, damit das Formular nach einem fehlgeschlagenen Absenden nicht leer erscheint, sondern die bereits eingegebenen Daten weiterhin anzeigt. Da Server Actions bei jedem Aufruf einen kompletten Roundtrip durchlaufen, wuerden unkontrollierte Formularfelder ohne diese Wertespeicherung nach einem Fehler ansonsten auf ihren initialen, meist leeren Zustand zurueckfallen, was fuer den Nutzer wie ein Datenverlust wirkt.


'use client';
import { useActionState } from 'react';
import { updateProfile } from './actions';

function ProfileForm({ userId }) {
  const [state, formAction, isPending] = useActionState(updateProfile, {
    success: false,
    errors: {},
    values: { email: '' },
    userId,
  });

  return (
    <form action={formAction}>
      <input
        name="email"
        defaultValue={state.values.email}
        aria-invalid={Boolean(state.errors.email)}
      />
      {state.errors.email && (
        <p className="text-sm text-red-600">{state.errors.email}</p>
      )}
      <button disabled={isPending}>
        {isPending ? 'Speichert ...' : 'Speichern'}
      </button>
    </form>
  );
}

4. Den Pending-Zustand sinnvoll kombinieren

Das dritte Rueckgabeelement von useActionState ist ein boolescher isPending-Wert, der automatisch waehrend der Ausfuehrung der Action true ist und danach wieder auf false zurueckspringt, sobald der neue State verfuegbar ist. Das erspart einen eigenen useState-Aufruf ausschliesslich fuer den Ladezustand und stellt sicher, dass Pending und Ergebnis niemals auseinanderlaufen koennen, was bei manueller State-Verwaltung ein haeufiger Fehlerquell ist, etwa wenn der Pending-Flag faelschlich schon vor dem tatsaechlichen Start der Anfrage zurueckgesetzt wird.

In der UI empfiehlt es sich, isPending nicht nur fuer einen Ladeindikator im Button zu nutzen, sondern auch, um Doppel-Submits zu verhindern, indem der Submit-Button waehrend der laufenden Aktion deaktiviert wird. Zusaetzlich kann isPending genutzt werden, um bestehende Fehlermeldungen aus dem vorherigen Versuch waehrend eines neuen Versuchs optisch abzuschwaechen, statt sie abrupt verschwinden zu lassen, was dem Nutzer signalisiert, dass gerade ein neuer Versuch laeuft, ohne die vorherige Information komplett zu verlieren.

5. Client-seitige Vorabvalidierung sinnvoll ergaenzen

useActionState ersetzt keine client-seitige Vorabvalidierung, sondern ergaenzt sie sinnvoll: Offensichtliche Fehler wie ein leeres Pflichtfeld oder ein falsches Zeichenformat lassen sich weiterhin mit den nativen HTML-Attributen required, pattern oder type sofort im Browser abfangen, ohne dass dafuer ein Server-Roundtrip noetig ist. Die Server Action bleibt dennoch die massgebliche, autoritative Instanz fuer die eigentliche Validierung, weil client-seitige Pruefungen sich umgehen lassen, etwa durch direkte Requests ohne Browser-UI.

In der Praxis bewaehrt sich eine zweistufige Strategie: schnelles, optimistisches Feedback ueber native HTML-Validierung fuer die haeufigsten Tippfehler, kombiniert mit der serverseitigen, autoritativen Pruefung ueber useActionState fuer alles, was tatsaechlich gegen die Datenbank oder externe Regeln geprueft werden muss, etwa ob eine E-Mail-Adresse bereits vergeben ist. Beide Ebenen schreiben dabei in dasselbe errors-Objekt im State, sodass die UI-Komponenten nicht zwischen den beiden Fehlerquellen unterscheiden muessen.

6. Unterschied zu manuellem try/catch im Handler

Ein klassischer Ansatz ohne useActionState wuerde einen eigenen async onSubmit-Handler schreiben, der e.preventDefault() aufruft, manuell einen isSubmitting-State auf true setzt, die Server-Funktion in einem try/catch aufruft, im catch-Block einen Fehlerstate setzt und im finally-Block isSubmitting wieder auf false setzt. Das funktioniert, erzeugt aber deutlich mehr Boilerplate, verteilt den Zustand ueber mehrere useState-Aufrufe und macht es leicht, den finally-Block oder eine Race-Condition-Absicherung bei schnellen Mehrfachklicks zu vergessen.

Ein weiterer struktureller Unterschied: try/catch faengt nur tatsaechlich geworfene Exceptions ab, waehrend useActionState auch mit Funktionen arbeitet, die Fehler als regulaeren Rueckgabewert modellieren, was fuer Validierungsfehler die semantisch passendere Wahl ist, da eine ungueltige E-Mail-Adresse kein Ausnahmefall im Sinne einer Exception ist, sondern ein erwartbarer, regulaerer Formularzustand. Echte Ausnahmefaelle, etwa ein Datenbankverbindungsfehler, lassen sich innerhalb der Action-Funktion weiterhin ganz normal mit try/catch behandeln und dann als strukturierter Fehler im zurueckgegebenen State abgebildet werden, statt die Exception ungefangen nach aussen durchzureichen.


// Manueller Ansatz ohne useActionState
function ProfileFormManual({ userId }) {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState(null);
  const [email, setEmail] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);
    try {
      await updateProfileClient(userId, email);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {error && <p>{error}</p>}
      <button disabled={isSubmitting}>Speichern</button>
    </form>
  );
}

7. Verschachtelte und globale Fehler gleichzeitig abbilden

Reale Formulare brauchen oft sowohl feldspezifische Fehler als auch globale Meldungen, etwa wenn ein Server-seitiger Fehler nicht auf ein einzelnes Feld zurueckzufuehren ist, zum Beispiel bei einem abgelaufenen Session-Token. Das State-Objekt von useActionState kann problemlos beide Ebenen gleichzeitig transportieren, indem neben dem errors-Objekt pro Feld ein zusaetzliches formError-Feld fuer solche uebergreifenden Meldungen existiert, das die Komponente separat oberhalb des eigentlichen Formulars anzeigt.

Fuer komplexere Formulare mit verschachtelten Feldern, etwa einer Adresse innerhalb eines Bestellformulars, bewaehrt es sich, das errors-Objekt in derselben Struktur wie die Formulardaten selbst aufzubauen, also state.errors.address.zipCode statt eines flachen Schluessels wie state.errors.addressZipCode. Das erleichtert es, die Fehlerbehandlung generisch ueber verschachtelte Komponenten zu reichen, ohne dass jede Ebene wissen muss, wie die Fehlerschluessel der jeweils anderen Ebene benannt sind.

8. Zusammenspiel mit useFormStatus in Kindkomponenten

Waehrend useActionState im Elternteil aufgerufen wird, das die Action und den State verwaltet, gibt es fuer tief verschachtelte Kindkomponenten, etwa einen wiederverwendbaren Submit-Button, den separaten Hook useFormStatus, der den Pending-Zustand des naechstgelegenen umschliessenden form-Elements liefert, ohne dass isPending explizit als Prop durchgereicht werden muss. Das ist besonders nuetzlich fuer generische UI-Komponenten, die in mehreren Formularen mit unterschiedlichen useActionState-Aufrufen wiederverwendet werden sollen.

useFormStatus muss dabei zwingend innerhalb des form-Elements aufgerufen werden, dessen Status es lesen soll, da es sich intern auf den naechstgelegenen Formular-Kontext bezieht und nicht auf eine explizit uebergebene Referenz. Ein Submit-Button, der useFormStatus nutzt, kann deshalb als eigenstaendige, formularunabhaengige Komponente exportiert und in beliebig vielen unterschiedlichen useActionState-Formularen eingesetzt werden, ohne dass die Formulare selbst Kenntnis von der internen Implementierung des Buttons haben muessen.


import { useFormStatus } from 'react-dom';

function SubmitButton({ children }) {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Wird gespeichert ...' : children}
    </button>
  );
}

// Verwendung in jedem beliebigen Formular:
function ProfileForm({ userId }) {
  const [state, formAction] = useActionState(updateProfile, initialState);
  return (
    <form action={formAction}>
      <input name="email" defaultValue={state.values.email} />
      <SubmitButton>Speichern</SubmitButton>
    </form>
  );
}

9. Wann sich useActionState wirklich lohnt

useActionState entfaltet seinen Nutzen vor allem bei Formularen mit echter serverseitiger Verarbeitung, insbesondere in Verbindung mit Server Actions in Frameworks wie Next.js, wo der Hook die native Formular-Semantik des Browsers respektiert und Formulare auch ohne aktives JavaScript funktionsfaehig bleiben, ein Vorteil, den ein reiner Client-Side-Ansatz mit fetch nicht bietet. Fuer rein clientseitige, sehr einfache Formulare ohne Server-Roundtrip kann ein simpler useState-Ansatz weiterhin ausreichend und sogar uebersichtlicher sein.

Bei mehreren gleichzeitig auf einer Seite existierenden Formularen, etwa in einer Tabelle mit Inline-Bearbeitung pro Zeile, sollte jede Zeile ihren eigenen useActionState-Aufruf mit eigenem initialen State erhalten, statt einen gemeinsamen State fuer alle Zeilen zu verwenden, da sonst der Pending-Zustand und die Fehlermeldungen einer Zeile faelschlich auch in anderen Zeilen erscheinen wuerden. Diese Isolation ist einer der Gruende, warum useActionState als Hook und nicht als globaler Store konzipiert wurde.

Aspekt useActionState Manuelles try/catch Empfehlung
Pending-Zustand Automatisch als drittes Rueckgabeelement Eigener useState noetig useActionState fuer weniger Boilerplate
Race Conditions bei Mehrfachklicks Von React intern abgesichert Muss manuell abgesichert werden useActionState fuer Formulare mit haeufigen Submits
Feldspezifische Fehler Strukturiertes errors-Objekt im State Mehrere separate useState-Aufrufe useActionState bei mehr als einem Feld
Funktioniert ohne JavaScript Ja, bei echten Server Actions Nein, benoetigt Client-Side JS useActionState fuer progressive Enhancement
Einfache Client-Only-Formulare Funktioniert, aber oft ueberdimensioniert Ausreichend und uebersichtlich try/catch fuer triviale Faelle

Mironsoft

React-Architektur, Performance und Magento-Frontend-Integration

React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?

Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.

Performance-Audit

Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.

State-Architektur

Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.

Magento-Integration

GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.

10. Zusammenfassung

useActionState Fehlerbehandlung: Das Wichtigste auf einen Blick

Drei Rueckgabewerte

useActionState liefert State, Wrapper-Action und isPending in einem einzigen, konsistenten Hook-Aufruf.

Feldfehler strukturiert

Das State-Objekt kann ein errors-Objekt pro Formularfeld enthalten statt einer globalen Fehlermeldung.

useFormStatus fuer Kinder

Tief verschachtelte Komponenten lesen den Pending-Status ueber useFormStatus, ohne Prop-Drilling.

Kein manuelles finally

React setzt isPending garantiert korrekt zurueck, unabhaengig von Erfolg oder Fehler der Action.

11. FAQ: useActionState Fehlerbehandlung: Das Wichtigste auf einen Blick

1Was ist der Unterschied zwischen useActionState und dem frueheren useFormState?
useFormState war der urspruengliche Name in fruehen React-19-Vorabversionen und wurde in useActionState umbenannt, weil der Hook nicht auf Formulare beschraenkt ist, sondern jede asynchrone Aktion mit strukturiertem Ergebnis-State verwalten kann. Die API und das Verhalten sind identisch geblieben, lediglich der Name hat sich geaendert.
2Muss die an useActionState uebergebene Funktion eine Server Action sein?
Nein, useActionState funktioniert grundsaetzlich mit jeder async Funktion, die dem erwarteten Signaturmuster aus vorherigem State und Argumenten folgt. Server Actions mit der use-server-Direktive sind ein haeufiger, aber nicht der einzige Anwendungsfall, der Hook laesst sich ebenso mit rein clientseitigen asynchronen Funktionen einsetzen.
3Wie zeige ich Validierungsfehler fuer einzelne Formularfelder an?
Man modelliert den von der Action zurueckgegebenen State als Objekt mit einem errors-Feld, das pro Formularfeldname eine eigene Fehlermeldung enthaelt. Die jeweilige Eingabekomponente liest dann gezielt state.errors.feldname aus und zeigt die Meldung direkt neben dem betroffenen Feld an, statt einer Sammelmeldung am Formularende.
4Warum verschwinden meine eingegebenen Werte nach einem fehlgeschlagenen Submit?
Das passiert, wenn die zuletzt eingegebenen Werte nicht im zurueckgegebenen State gespeichert werden. Die Loesung ist, die Formulardaten als values-Feld im State-Objekt mitzufuehren und die Eingabefelder mit defaultValue an diese gespeicherten Werte zu binden, damit sie nach einem Fehler erhalten bleiben.
5Kann ich isPending nutzen, um Doppel-Submits zu verhindern?
Ja, das ist einer der Hauptanwendungsfaelle. Der Submit-Button sollte waehrend isPending true deaktiviert werden, wodurch verhindert wird, dass ein Nutzer durch mehrfaches schnelles Klicken dieselbe Action mehrfach gleichzeitig ausloest, was ohne diese Absicherung zu inkonsistenten Serverzustaenden fuehren koennte.
6Was ist der Vorteil gegenueber einem eigenen try/catch im Submit-Handler?
useActionState reduziert Boilerplate, sichert Race Conditions bei schnellen Mehrfachklicks automatisch ab und modelliert Validierungsfehler als regulaeren Rueckgabewert statt als geworfene Exception, was semantisch praeziser ist. Echte Ausnahmefaelle lassen sich weiterhin mit try/catch innerhalb der Action-Funktion behandeln und dann als strukturierter Fehler im State abgebildet werden.
7Wofuer ist useFormStatus im Vergleich zu useActionState gedacht?
useFormStatus liest den Pending-Status eines umschliessenden form-Elements aus einer beliebig tief verschachtelten Kindkomponente aus, ohne dass isPending explizit als Prop durchgereicht werden muss. useActionState dagegen wird im Elternteil aufgerufen, das die Action selbst besitzt und den vollstaendigen Ergebnis-State verwaltet.
8Wie behandle ich globale Fehler, die keinem einzelnen Formularfeld zugeordnet werden koennen?
Man ergaenzt das State-Objekt um ein zusaetzliches Feld, etwa formError, das unabhaengig vom errors-Objekt fuer einzelne Felder existiert. Die Formularkomponente zeigt diesen globalen Fehler dann separat oberhalb oder unterhalb der eigentlichen Eingabefelder an.
9Brauche ich fuer jedes Formular auf einer Seite einen eigenen useActionState-Aufruf?
Ja, bei mehreren unabhaengigen Formularen auf derselben Seite, etwa in einer Tabelle mit Inline-Bearbeitung, sollte jedes Formular seinen eigenen useActionState-Aufruf mit eigenem initialen State erhalten. Ein geteilter State wuerde sonst dazu fuehren, dass Fehler und Pending-Zustand einer Zeile faelschlich auch in anderen Zeilen sichtbar werden.
10Lohnt sich useActionState auch fuer sehr einfache, rein clientseitige Formulare?
Nicht unbedingt. Fuer triviale Formulare ohne Server-Roundtrip und ohne Notwendigkeit fuer Funktionsfaehigkeit ohne JavaScript kann ein einfacher useState-Ansatz uebersichtlicher und ausreichend sein. useActionState zeigt seinen vollen Nutzen vor allem bei echten Server Actions mit mehreren Feldern und dem Wunsch nach progressiver Verbesserung.