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.
Inhaltsverzeichnis
- 1. Was useActionState loest und woher der Name kommt
- 2. Grundstruktur: Action-Funktion und Formular verbinden
- 3. Validierungsfehler pro Feld strukturiert zurueckgeben
- 4. Den Pending-Zustand sinnvoll kombinieren
- 5. Client-seitige Vorabvalidierung sinnvoll ergaenzen
- 6. Unterschied zu manuellem try/catch im Handler
- 7. Verschachtelte und globale Fehler gleichzeitig abbilden
- 8. Zusammenspiel mit useFormStatus in Kindkomponenten
- 9. Wann sich useActionState wirklich lohnt
- 10. Zusammenfassung
- 11. FAQ
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.