React Actions: Formulare ohne useState und Handler
AI generated
</>
{ }
React Actions · Server Actions · useActionState · Forms
React Actions: Formulare
ohne useState und ohne Handler

Ein React-Formular mit Validierung, Ladezustand und Fehlerbehandlung brauchte in React 18 mindestens fünf useState-Hooks, einen onSubmit-Handler, einen fetch-Aufruf und manuelle Rollback-Logik. React Actions in React 19 erledigen dasselbe mit einem einzigen Hook und einer Action-Funktion – und funktionieren auch ohne JavaScript im Browser.

16 Min. Lesezeit useActionState · Server Actions · useFormStatus · useOptimistic · FormData React 19 · Next.js 15 · TypeScript

1. Das Formular-Problem in React 18

Formulare sind in React 18 unverhältnismäßig aufwändig. Ein einfaches Kontaktformular mit Validierung, Ladezustand und Fehlerbehandlung erfordert typischerweise: useState für jeden Feldwert, useState für Validierungsfehler, useState für den Ladezustand, useState für den Erfolgszustand und einen onSubmit-Handler, der den Browser-Default verhindert, alle Felder validiert, einen fetch-Aufruf mit try/catch macht, den Ladezustand setzt und zurücksetzt, und bei Erfolg das Formular zurücksetzt. Das ist mindestens 40–60 Zeilen Code für einen Vorgang, der konzeptuell einfach ist: Daten zum Server schicken und auf das Ergebnis reagieren.

React Actions lösen dieses Problem strukturell. Statt State und Handler getrennt zu verwalten, fasst das React Actions-Modell den gesamten Lifecycle – Absenden, Warten, Fehler, Erfolg – in einer einzigen Abstraktion zusammen. Der useActionState-Hook übernimmt State-Management und Lifecycle-Koordination. Server Actions übernehmen die serverseitige Ausführung ohne separate API-Routen. useFormStatus macht den Pending-Zustand in Kindkomponenten sichtbar. Das Ergebnis: weniger Code, konsistentere Fehlerbehandlung und progressive Enhancement ohne zusätzlichen Aufwand.

2. Was sind React Actions?

React Actions sind Funktionen, die als action-Prop eines Formular-Elements oder Button-Elements übergeben werden. React übernimmt automatisch das Pending-State-Management, während die Action ausgeführt wird. Eine React Action kann synchron oder asynchron sein. Sie empfängt ein FormData-Objekt mit allen Formularfeldern und gibt optional einen neuen State zurück. Das ist das grundlegende Modell: HTML-nah, deklarativ und ohne explizite Event-Handler.

Der konzeptuelle Unterschied zu onSubmit: onSubmit ist ein JavaScript-Event-Handler, der nur mit JavaScript funktioniert und manuell e.preventDefault() aufrufen muss. Eine React Action ist semantisch näher am HTML-action-Attribut: Sie ist eine Funktion, die den Formular-Submit-Vorgang repräsentiert, unabhängig davon, ob JavaScript verfügbar ist. React 19 nutzt dieses Modell, um progressives Enhancement einzubauen: Ohne JavaScript sendet der Browser das Formular per Standard-POST. Mit JavaScript übernimmt React die Action und führt sie ohne Seiten-Reload aus. React Actions sind damit der sauberere, modernere Ersatz für alle bisherigen Formular-Submission-Patterns in React.

3. useActionState: der zentrale Hook für React Actions

useActionState ist der Hook, der React Actions mit State verbindet. Er nimmt zwei Argumente entgegen: die Action-Funktion und einen initialen State. Er gibt drei Werte zurück: den aktuellen State (der sich nach jeder Action-Ausführung aktualisiert), eine wrapped Version der Action (die dem Formular übergeben wird) und ein isPending-Boolean, der während der Ausführung der Action true ist. Die Action-Funktion selbst empfängt als erstes Argument den vorherigen State, als zweites die FormData. Das ermöglicht akkumulierende Patterns wie Optimistic Updates oder stateabhängige Validierung.

Der State von useActionState wird vom Server zurückgegeben, wenn die Action eine Server Action ist. Das bedeutet: Die Server Action gibt einen serialisierbaren Wert zurück – etwa ein Objekt mit { success: boolean; errors: Record<string, string>; data: ... } – und dieser Wert wird nach der Ausführung automatisch als neuer State in der Komponente verfügbar. Es gibt keine manuelle setState-Aufrufe, keinen separaten Error-State und kein manuelles Setzen des Loading-Indikators. React Actions mit useActionState sind damit das deklarativste Formular-Pattern, das React je hatte.


'use client';
import { useActionState } from 'react';
import { submitContact } from './actions'; // Server Action

// Define state shape for type safety
interface ContactState {
  success: boolean;
  message: string;
  errors: { name?: string; email?: string; message?: string };
}

const initialState: ContactState = {
  success: false,
  message: '',
  errors: {},
};

export function ContactForm() {
  // useActionState: connects the Server Action with local state
  const [state, action, isPending] = useActionState(submitContact, initialState);

  if (state.success) {
    return (
      <div className="bg-green-50 border border-green-200 rounded-xl p-6">
        <p className="text-green-800 font-semibold">{state.message}</p>
      </div>
    );
  }

  return (
    // action prop receives the wrapped Server Action — no onSubmit needed
    <form action={action} noValidate>
      <div>
        <label htmlFor="name">Name</label>
        <input id="name" name="name" required />
        {/* Inline field errors from server-side validation */}
        {state.errors.name && (
          <p className="text-red-600 text-sm">{state.errors.name}</p>
        )}
      </div>
      <div>
        <label htmlFor="email">E-Mail</label>
        <input id="email" name="email" type="email" required />
        {state.errors.email && (
          <p className="text-red-600 text-sm">{state.errors.email}</p>
        )}
      </div>
      <div>
        <label htmlFor="message">Nachricht</label>
        <textarea id="message" name="message" required />
        {state.errors.message && (
          <p className="text-red-600 text-sm">{state.errors.message}</p>
        )}
      </div>
      {state.message && !state.success && (
        <p className="text-red-600">{state.message}</p>
      )}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Wird gesendet...' : 'Nachricht senden'}
      </button>
    </form>
  );
}

4. Server Actions: Logik auf dem Server ausführen

Server Actions sind Funktionen, die mit der 'use server'-Direktive markiert werden und auf dem Server laufen, obwohl sie von Client-Code aufgerufen werden. Im Kontext von React Actions sind Server Actions das serverseitige Gegenstück zu useActionState: Sie empfangen FormData, führen Validierung, Datenbankoperationen und Business-Logik aus, und geben einen neuen State zurück. Der Rückgabewert muss serialisierbar sein – also ein JSON-kompatibler Wert. Der Server sendet diesen Wert zurück an den Client, wo React ihn als neuen State in useActionState einsetzt.

Der wichtigste Vorteil von Server Actions als Teil von React Actions: Es gibt keine separate API-Route, keinen separaten Controller, kein separates Routing-Setup. Die Action ist direkt an die Komponente gebunden, lebt in einer Datei, die semantisch zu der Feature gehört, und ist trotzdem serverseitig ausgeführt. Das verbessert die Code-Lokalität erheblich: Validierung, Datenbankzugriff und UI-State-Management für dasselbe Formular sind in derselben Datei – eine für Server, eine für Client – statt über API-Routes, Controllers und Frontend-Services verteilt.

5. useFormStatus: Submit-Zustand in Kindkomponenten

useFormStatus ist ein React-19-Hook aus react-dom, der den Pending-Zustand des nächstgelegenen Eltern-Formulars aus einer Kindkomponente heraus lesen kann. Das klingt spezifisch, löst aber ein häufiges Problem bei React Actions: Der Submit-Button soll deaktiviert werden, während die Action läuft – aber der Button lebt in einer separaten Komponente, und den Pending-Zustand als Prop nach unten zu reichen bedeutet prop drilling.

useFormStatus gibt ein Objekt mit { pending, data, method, action } zurück. Das pending-Flag ist true, solange das übergeordnete Formular eine Action ausführt. data enthält das aktuelle FormData-Objekt des Formulars – nützlich, um im Button anzuzeigen, was gerade abgesendet wird. Wichtig: useFormStatus muss in einer Komponente verwendet werden, die ein Kind des Formulars ist – nicht in der Komponente, die das Formular rendert. Das ist eine häufige Fehlerquelle beim Einsatz von React Actions.


'use server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

// Zod schema for type-safe server-side validation
const ContactSchema = z.object({
  name: z.string().min(2, 'Name muss mindestens 2 Zeichen haben'),
  email: z.string().email('Ungültige E-Mail-Adresse'),
  message: z.string().min(10, 'Nachricht muss mindestens 10 Zeichen haben'),
});

interface ContactState {
  success: boolean;
  message: string;
  errors: { name?: string; email?: string; message?: string };
}

// Server Action — receives prevState from useActionState and FormData
export async function submitContact(
  prevState: ContactState,
  formData: FormData
): Promise<ContactState> {
  // Parse FormData into a plain object for Zod validation
  const raw = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  };

  const parsed = ContactSchema.safeParse(raw);

  if (!parsed.success) {
    // Return field-level errors — client renders them next to each input
    return {
      success: false,
      message: 'Bitte alle Felder korrekt ausfüllen.',
      errors: parsed.error.flatten().fieldErrors as ContactState['errors'],
    };
  }

  try {
    await db.query(
      'INSERT INTO contacts (name, email, message) VALUES ($1, $2, $3)',
      [parsed.data.name, parsed.data.email, parsed.data.message]
    );
    revalidatePath('/kontakt');
    return { success: true, message: 'Nachricht erfolgreich gesendet!', errors: {} };
  } catch {
    return { success: false, message: 'Serverfehler – bitte versuche es später.', errors: {} };
  }
}

6. Validierung mit Zod in React Actions

Serverseitige Validierung ist bei React Actions mit Zod elegant lösbar. Zod's safeParse-Methode gibt entweder die validierten Daten oder ein strukturiertes Fehler-Objekt zurück. Mit error.flatten().fieldErrors erhält man ein Objekt, das jeden Feldnamen auf ein Array von Fehlermeldungen mapped – genau das Format, das die Client-Komponente für die feldbezogene Fehleranzeige braucht. Die Typen werden zwischen Server Action und Client-Komponente geteilt, sodass TypeScript die Vollständigkeit der Fehlerbehandlung prüft.

Das Muster für zweistufige Validierung mit React Actions: clientseitige HTML5-Validierung mit required, type="email" und minLength für sofortiges Feedback ohne Server-Roundtrip, kombiniert mit serverseitiger Zod-Validierung in der Server Action für sicherheitsrelevante Prüfungen. Die Server Action darf niemals auf clientseitige Validierung vertrauen – alle Daten müssen serverseitig geprüft werden, da ein Angreifer den Browser-Client umgehen kann. Mit React Actions und Zod ist das deutlich weniger Boilerplate als mit dem alten onSubmit-Pattern, wo Validierungslogik oft redundant in Client- und Server-Code existierte.

7. useOptimistic: sofortiges Feedback ohne Warten

useOptimistic ist der dritte wichtige Hook im React-Actions-Ökosystem. Er ermöglicht es, sofort einen optimistischen Wert anzuzeigen, bevor die Server Action abgeschlossen ist. Das Muster: Man ruft const [optimisticList, addOptimistic] = useOptimistic(serverList) auf, zeigt optimisticList in der UI an, und ruft addOptimistic(newItem) auf, direkt bevor die React Action abgesendet wird. React zeigt sofort die Liste mit dem neuen Item – ohne auf den Server zu warten. Wenn die Server Action erfolgreich ist, wird der Server-Wert übernommen. Wenn sie fehlschlägt, stellt React automatisch den vorherigen Wert wieder her.

Das Anwendungsbeispiel für useOptimistic im React-Actions-Kontext: Eine Todo-Liste, bei der neue Einträge sofort erscheinen sollen, ohne auf das Datenbankschreiben zu warten. Ein Like-Button, der sofort den geklickten Zustand zeigt, auch wenn das Backend noch nicht geantwortet hat. Ein Kommentar-Formular, das den neuen Kommentar sofort in der Liste anzeigt. In allen diesen Fällen verbessert useOptimistic die wahrgenommene Performance erheblich – die UI fühlt sich sofort reaktionsschnell an, und Fehler werden transparent durch automatisches Rollback kommuniziert.

8. Progressive Enhancement mit React Actions

Progressive Enhancement ist ein Architektur-Prinzip: Eine Anwendung soll mit minimalem JavaScript funktionieren und durch JavaScript enhanced werden, nicht auf JavaScript angewiesen sein. React Actions unterstützen dieses Prinzip nativ. Ein Formular mit einer Server Action als action-Prop sendet bei fehlendem JavaScript einen normalen Browser-POST an den Server – und der Server verarbeitet die Anfrage und liefert eine neue Seite mit dem aktualisierten State. Mit JavaScript übernimmt React die Action, führt sie asynchron aus und aktualisiert nur den betroffenen Teil der UI ohne Seiten-Reload.

Das bedeutet für die Praxis: React Actions funktionieren ohne JavaScript-Ladezeit, ohne Hydration und ohne Bundle-Größe als Voraussetzung. Das ist besonders relevant für Formulare, die suchmaschinenindexiert werden sollen, für Seiten mit vielen Nutzern auf langsamen Verbindungen und für Forms in Server-gerenderten Seiten. Das bisherige Muster mit onSubmit bot keine progressive Enhancement – ohne JavaScript war das Formular komplett nicht funktionsfähig. React Actions kehren dieses Verhältnis um: ohne JavaScript funktioniert, mit JavaScript performt besser.


'use client';
import { useActionState, useOptimistic } from 'react';
import { addTodo, toggleTodo } from './actions';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
  pending?: boolean; // optimistic flag
}

interface TodoState {
  todos: Todo[];
  error: string | null;
}

export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
  const [state, action, isPending] = useActionState(addTodo, {
    todos: initialTodos,
    error: null,
  });

  // useOptimistic: show immediate feedback before server responds
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    state.todos,
    // Reducer: how to merge optimistic update with current state
    (currentTodos, newTodoText: string) => [
      ...currentTodos,
      { id: crypto.randomUUID(), text: newTodoText, completed: false, pending: true },
    ]
  );

  return (
    <div>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={ { opacity: todo.pending ? 0.6 : 1 } }>
            <span style={ { textDecoration: todo.completed ? 'line-through' : 'none' } }>
              {todo.text}
            </span>
            {todo.pending && <span> (wird gespeichert...)</span>}
          </li>
        ))}
      </ul>

      {/* Form with action — works without JavaScript too */}
      <form
        action={async (formData) => {
          const text = formData.get('todo') as string;
          // Immediately show optimistic update before server responds
          addOptimisticTodo(text);
          await action(formData);
        } }
      >
        <input name="todo" required placeholder="Neues Todo..." />
        <button type="submit" disabled={isPending}>Hinzufügen</button>
      </form>

      {state.error && <p className="text-red-600">{state.error}</p>}
    </div>
  );
}

9. React 18 Formular-Pattern vs. React Actions im Vergleich

Der direkte Vergleich zeigt, wie viel Boilerplate React Actions eliminieren und wo die neuen Patterns klar gewinnen.

Aspekt React 18 Pattern React Actions (React 19) Gewinn
Ladezustand useState(false) + manuelles setLoading isPending aus useActionState Kein manuelles State-Management
Fehlerzustand useState(null) + try/catch im Handler Rückgabewert der Server Action Server-seitig, typsicher, kein Race Condition
Formular-Submit onSubmit + e.preventDefault() action-Prop, kein preventDefault Progressive Enhancement eingebaut
API-Kommunikation fetch + eigene Route + Serialisierung Server Action, kein API-Route Weniger Dateien, bessere Lokalität
Optimistic UI Manuelle setState + Rollback-Logik useOptimistic mit auto-Rollback Automatisches Rollback bei Fehler
Kein JavaScript Formular komplett kaputt Browser-POST funktioniert Zugänglicher, robuster

Die Tabelle macht deutlich: React Actions sind kein kosmetisches Update. Sie verändern das Formular-Paradigma grundlegend. Die Codeeinsparung ist messbar: Das Beispiel-Kontaktformular von Abschnitt 1 würde in React 18 etwa 80 Zeilen Code benötigen. Mit React Actions und useActionState sind es etwa 40 – bei mehr Funktionalität, besserer Fehlerbehandlung und progressivem Enhancement.

Mironsoft

React-Entwicklung, Server Actions und moderne Formular-Architektur

React-Formulare modernisieren?

Wir migrieren bestehende React-18-Formulare auf React Actions, führen Server Actions ein, integrieren Zod-Validierung und implementieren progressive Enhancement für bessere Zugänglichkeit und Resilienz.

Forms-Audit

Analyse bestehender Formulare auf Boilerplate, Race Conditions und fehlende Validierung

Server Actions

Einführung von Server Actions mit Zod-Validierung und typsicheren States

Optimistic UI

useOptimistic für sofortiges Feedback mit automatischem Rollback implementieren

10. Zusammenfassung

React Actions sind die fundamentale Neuerung für Formulare in React 19. Das alte Pattern aus fünf useState-Hooks, einem onSubmit-Handler, einem fetch-Aufruf und manueller Rollback-Logik wird durch useActionState mit einer Server Action ersetzt. useFormStatus macht den Pending-Zustand in Kindkomponenten sichtbar, ohne Prop Drilling. useOptimistic liefert sofortiges Feedback mit automatischem Rollback. Progressive Enhancement ist kein Zusatzaufwand mehr, sondern eine eingebaute Eigenschaft des React Actions-Modells.

Die praktische Konsequenz: Wer React Actions in einem React-19-Projekt mit Next.js 15 oder Remix einsetzt, schreibt Formulare mit etwa der Hälfte des bisherigen Codes – bei besserer Typsicherheit, robusterer Fehlerbehandlung und ohne Race Conditions zwischen konkurrierenden State-Updates. Die Einführung von Zod für Server-seitige Validierung ist dabei der natürliche nächste Schritt und fügt sich nahtlos in das React Actions-Pattern ein. Wer bestehende React-18-Formulare modernisieren will, sollte mit dem Austausch des onSubmit-Handlers gegen eine Server Action beginnen – der Rest folgt strukturell.

React Actions — Das Wichtigste auf einen Blick

useActionState

Verbindet Action-Funktion mit State. Gibt currentState, wrappedAction und isPending zurück. Ersetzt useState für Lade- und Fehlerzustand vollständig.

Server Actions

'use server' markierte Funktion – läuft auf dem Server, empfängt FormData, gibt serialisierbaren State zurück. Kein API-Route nötig.

useFormStatus + useOptimistic

useFormStatus liest pending in Kindkomponenten. useOptimistic zeigt sofortige Werte mit automatischem Rollback bei Fehler.

Progressive Enhancement

Formular mit Server Action als action-Prop funktioniert ohne JavaScript per Browser-POST. Mit JavaScript: kein Seiten-Reload, volle Interaktivität.

11. FAQ: React Actions – Formulare ohne useState und Handler

1React Action vs. onSubmit-Handler?
onSubmit braucht JavaScript und e.preventDefault(). React Action funktioniert auch ohne JS per Browser-POST. Semantisch näher an HTML-action-Attribut.
2Immer ein Server nötig?
Nein. Client Actions laufen im Browser – keine Server-Unterstützung nötig. Server Actions erfordern Next.js 15+, Remix oder TanStack Start.
3Wie funktioniert useActionState?
Empfängt Server Action und initialen State. Gibt currentState, wrappedAction und isPending zurück. Action-Rückgabewert wird neuer State.
4Ohne Next.js verwendbar?
Client Actions ja. Server Actions benötigen Framework-Support. Next.js 15, Remix und TanStack Start unterstützen Server Actions.
5Serverseitige Validierung mit Zod?
FormData in Objekt umwandeln, Zod safeParse, bei Fehler fieldErrors zurückgeben. Client rendert Fehler feldbezogen aus dem State.
6Wo useFormStatus verwenden?
In einer Kindkomponente des Formulars – nicht in der Formular-Komponente selbst. Typisch: separater SubmitButton-Component.
7useOptimistic bei Fehler?
Automatisches Rollback. Optimistischer Wert verschwindet, Server-Wert (unverändert) wird wiederhergestellt. Kein manuelles Rollback nötig.
8Progressive Enhancement automatisch?
Ja, wenn Server Action als action-Prop und Framework progressive Enhancement unterstützt. Ohne JS: Browser-POST funktioniert.
9Mehrere Actions in einer Komponente?
Ja. Jeder useActionState-Aufruf ist unabhängig. Mehrere Formulare mit unterschiedlichen Actions sind problemlos möglich.
10Server Actions testen?
Direkt mit Jest oder Vitest testbar – normale async-Funktionen. FormData im Test erstellen: new FormData(). Rückgabewert gegen State-Shape prüfen.