</>
{ }
React 19 · Server Actions · Hooks · Formulare
React useFormStatus
Lade-Feedback für verschachtelte Formulare

Einen Submit-Button in einer tief verschachtelten Formular-Komponente zu deaktivieren, während das Formular abgeschickt wird, erfordert bislang Prop-Drilling oder externen State. useFormStatus macht diesen Zustand ohne beides zugänglich.

10 Min. Lesezeit useFormStatus · Server Actions · pending · FormData · Next.js React 19+ · TypeScript

1. Das Prop-Drilling-Problem bei Formularen

In typischen React-Formular-Implementierungen gibt es ein wiederkehrendes Problem: Der Submit-Button befindet sich tief in der Komponenten-Hierarchie, der Pending-Status aber wird in der Eltern-Komponente verwaltet. Um den Button zu deaktivieren, während das Formular abgeschickt wird, muss isSubmitting als Prop durch alle Zwischenschichten gereicht werden. Dieses Prop-Drilling erschwert Refactoring, macht Komponenten starr und koppelt Design-Entscheidungen an Implementierungsdetails.

Das klassische Gegenmodell war, den Pending-State in einen globalen Store (Redux, Zustand, Jotai) auszulagern und in der Kind-Komponente zu abonnieren. Das löst das Prop-Drilling-Problem, führt aber zu einer unnötigen Kopplung von UI-Komponenten an den globalen State-Layer. Ein einfaches Formular braucht keine globale State-Lösung für einen simplen Lade-Spinner. useFormStatus löst genau dieses Problem auf die native React-Art: Der Status des nächsten Formular-Ancestors wird direkt im Kind zugänglich, ohne Prop-Drilling und ohne globalen State.

Besonders relevant wird useFormStatus in der Ära der Server Actions. React 19 und Next.js 14+ erlauben es, Formular-action-Attribute auf Server-Funktionen zu setzen. Das Formular wird abgeschickt, die Server-Funktion verarbeitet die Daten, React aktualisiert den Zustand. Während der Server-Funktion läuft, ist das Formular im Pending-Zustand – genau das macht useFormStatus für Kind-Komponenten sichtbar.

2. Server Actions: Formulare ohne JavaScript-Handler

Server Actions sind ein Kernfeature von React 19, das in Next.js 14 (App Router) breite Anwendung findet. Anstatt einen onSubmit-Handler zu schreiben, der eine API-Route aufruft, wird eine Funktion direkt als action-Prop des Formulars gesetzt. Diese Funktion wird serverseitig ausgeführt, empfängt das FormData-Objekt und kann direkt auf Datenbanken zugreifen, ohne eine separate API-Schicht. Das Formular funktioniert sogar ohne JavaScript, wenn der Browser Formulare nativ abschickt.

Der Pending-Zustand während einer Server Action wird von React intern verwaltet. Das Formular weiß, dass eine Aktion läuft, und setzt intern eine Pending-Flag. useFormStatus stellt diese Flag als pending: boolean bereit. In Client-Komponenten, die innerhalb des Formulars gerendert werden, kann dieser Status direkt gelesen und für UI-Feedback genutzt werden – ohne dass der Formular-Container diesen Status als Prop weiterreichen muss.

Wichtig zu verstehen: Server Actions funktionieren auch mit Client-seitigem State und useActionState (früher useFormState). Das Zusammenspiel dieser Hooks bildet das neue Formular-Ökosystem von React 19: useActionState verwaltet den Ergebnis-State nach der Aktion, useFormStatus liefert den Status während der Aktion, und useOptimistic ermöglicht optimistische Updates. Alle drei greifen ineinander und ergänzen sich.

3. useFormStatus: Syntax und verfügbare Felder

useFormStatus ist ein Hook aus dem react-dom-Paket (nicht aus react). Er gibt ein Objekt mit vier Feldern zurück: pending ist ein Boolean, der anzeigt, ob das übergeordnete Formular gerade abgeschickt wird. data ist ein FormData-Objekt mit den übermittelten Daten, oder null wenn kein Pending-Zustand besteht. method ist die HTTP-Methode des Formulars (get oder post). action ist die action-Prop des Formulars – kann eine URL-String oder eine Funktion (Server Action) sein.

Die Schnittstelle ist bewusst einfach gehalten. In den meisten Anwendungsfällen wird nur pending benötigt. Die übrigen Felder sind für fortgeschrittene Szenarien vorgesehen, etwa wenn eine Kind-Komponente im Pending-Zustand die übermittelten Daten anzeigen möchte (optimistische UI). Der Hook ist ein reiner "Read"-Hook: Er verändert keinen State und hat keine Seiteneffekte.


'use client';

import { useFormStatus } from 'react-dom';

// SubmitButton must be a CHILD of the form element — not in the same component
function SubmitButton({ label = 'Speichern' }: { label?: string }) {
  // Reads status from the nearest parent <form>
  const { pending, data, method } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      aria-disabled={pending}
      className={`px-4 py-2 rounded font-semibold transition-opacity ${
        pending ? 'opacity-60 cursor-not-allowed' : 'opacity-100'
      }`}
    >
      {pending ? (
        <span className="flex items-center gap-2">
          {/* Inline spinner — no external dependency */}
          <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
            <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" />
            <path fill="currentColor" d="M4 12a8 8 0 018-8v8z" className="opacity-75" />
          </svg>
          Wird gesendet …
        </span>
      ) : (
        label
      )}
    </button>
  );
}

// Server action — runs on the server
async function saveContactForm(formData: FormData) {
  'use server';
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  // Database or email dispatch here
  await new Promise((r) => setTimeout(r, 1500)); // Simulate latency
}

export function ContactForm() {
  return (
    // action receives the Server Action function
    <form action={saveContactForm} className="space-y-4">
      <input name="name" type="text" placeholder="Name" required />
      <input name="email" type="email" placeholder="E-Mail" required />
      {/* SubmitButton reads pending from this form */}
      <SubmitButton label="Kontakt senden" />
    </form>
  );
}

4. Pending-Button: Der häufigste Anwendungsfall

Der bei weitem häufigste Anwendungsfall von useFormStatus ist ein Submit-Button, der sich während des Pending-Zustands deaktiviert und visuelles Feedback zeigt. Das Muster ist simpel: Eine eigene SubmitButton-Komponente liest pending via useFormStatus und setzt disabled={pending}. Diese Komponente kann in beliebig vielen Formularen wiederverwendet werden, ohne dass jedes Formular den Pending-Status managen muss.

Wichtig dabei: Die SubmitButton-Komponente muss ein Kind des <form>-Elements sein – sie kann nicht in derselben Komponente wie das <form>-Element stehen. Das ist die fundamentale Einschränkung von useFormStatus: Der Hook kann nur auf den Status eines Parent-Formulars zugreifen, nicht auf das Formular in derselben Komponente. Wenn man useFormStatus in der Komponente aufruft, die auch das <form>-Element rendert, gibt pending immer false zurück.

Der Button sollte neben disabled auch aria-disabled setzen und ggf. aria-label aktualisieren, um den Zustand für Screen-Reader-Nutzer verständlich zu machen. Ein Spinner-Icon ohne Text ist nicht ausreichend zugänglich – der Text sollte sich von "Speichern" zu "Wird gesendet …" ändern, damit auch assistive Technologien den Zustandswechsel wahrnehmen.

5. Formular-Daten im Pending-Zustand lesen

Das data-Feld von useFormStatus liefert das FormData-Objekt mit den gerade übermittelten Werten. Das ermöglicht es, bereits während des Ladevorgangs die eingegebenen Daten anzuzeigen – zum Beispiel den Namen des Nutzers im Feedback-Text. Dieses Muster ist ein einfaches optimistisches UI-Muster: Die UI zeigt die übermittelten Werte an, noch bevor die Serverantwort eintrifft.

Ein konkretes Beispiel: Ein Kommentar-Formular, das nach dem Abschicken sofort den neuen Kommentar in der Liste anzeigt, während der Server noch antwortet. Das data-Feld enthält den Kommentar-Text (data.get('comment')), und die Kind-Komponente kann diesen Wert direkt anzeigen. Sobald die Server-Antwort eintrifft und React re-rendert, wird der echte Server-State angezeigt und der optimistische State verworfen. Für aufwändigere optimistische Updates ist useOptimistic besser geeignet, aber für einfache Fälle reicht data aus.


'use client';

import { useFormStatus } from 'react-dom';
import { useActionState } from 'react';

// Reads submitted form data during pending state
function CommentPreview() {
  const { pending, data } = useFormStatus();

  if (!pending || !data) return null;

  const comment = data.get('comment') as string;
  const author = data.get('author') as string;

  // Show optimistic preview while server processes the request
  return (
    <div className="border border-sky-200 bg-sky-50 rounded-lg p-4 mt-4 opacity-60">
      <p className="text-xs text-sky-600 font-semibold mb-1">Wird gespeichert …</p>
      <p className="font-semibold text-sm">{author}</p>
      <p className="text-sm text-slate-600">{comment}</p>
    </div>
  );
}

// Reusable submit button with pending feedback
function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending} className="btn-primary">
      {pending ? 'Kommentar wird gespeichert …' : 'Kommentar absenden'}
    </button>
  );
}

// Server action
async function addComment(
  previousState: { success: boolean } | null,
  formData: FormData
) {
  'use server';
  const comment = formData.get('comment') as string;
  const author = formData.get('author') as string;
  if (!comment || !author) return { success: false };
  // Save to database
  await new Promise((r) => setTimeout(r, 1200));
  return { success: true };
}

export function CommentForm() {
  const [state, formAction] = useActionState(addComment, null);

  return (
    <div>
      {state?.success && (
        <p className="text-green-600 font-semibold">Kommentar gespeichert!</p>
      )}
      <form action={formAction} className="space-y-3">
        <input name="author" type="text" placeholder="Dein Name" required />
        <textarea name="comment" placeholder="Dein Kommentar …" required />
        <SubmitButton />
        {/* Renders optimistic preview during pending state */}
        <CommentPreview />
      </form>
    </div>
  );
}

6. Optimistische UI mit useFormStatus

Optimistische UI bedeutet, dass die Anwendung so tut, als wäre eine Aktion bereits erfolgreich abgeschlossen, noch bevor die Serverantwort vorliegt. Das macht Interaktionen gefühlt sofortig und erhöht die wahrgenommene Performance deutlich. useFormStatus und das data-Feld bilden die Basis für einfache optimistische Updates: Während des Pending-Zustands werden die übermittelten Daten direkt aus FormData gelesen und in der UI dargestellt.

Für komplexere Szenarien, bei denen der optimistische State vom tatsächlichen State abweichen kann und gezielt rückgängig gemacht werden muss, ist useOptimistic die richtige Wahl. Dieses Hook aus React 19 erlaubt es, einen temporären optimistischen State zu setzen, der automatisch durch den echten State ersetzt wird, sobald die Aktion abgeschlossen ist. useFormStatus und useOptimistic ergänzen sich: useFormStatus für den Pending-Zustand, useOptimistic für den vorläufigen Inhalt.

Ein Beispiel aus der Praxis: Eine Todo-Liste, bei der ein neues Todo sofort in der Liste erscheint, sobald der Nutzer auf "Hinzufügen" klickt – ohne auf die Serverantwort zu warten. Mit useOptimistic wird das neue Todo dem State hinzugefügt, mit einer "saving"-Markierung versehen und ausgegraut angezeigt. Wenn die Server Action erfolgreich ist, wird das echte Todo in den State integriert. Schlägt die Aktion fehl, verschwindet das optimistische Todo und eine Fehlermeldung erscheint.

7. useFormStatus vs. manuellem State im Vergleich

Vor React 19 war der Standard-Ansatz für Formular-Feedback: Ein isSubmitting-State in der Form-Komponente verwalten, ihn im onSubmit-Handler auf true setzen, nach dem Await auf false zurücksetzen, und als Prop an den Submit-Button weitergeben. Dieses Muster ist verständlich, erfordert aber Boilerplate und koppelt den Submit-Button-Stil an die Form-Implementierung.

Aspekt Manueller isSubmitting-State useFormStatus
Prop-Drilling Nötig für tiefe Hierarchien Kein Prop-Drilling
Wiederverwendbarkeit Button kennt Form-Implementierung Button ist komplett unabhängig
Server Actions Kein nativer Support Nativ integriert
Boilerplate useState, Handler, Cleanup Ein Hook-Aufruf
Formular-Daten Separater State nötig Via data-Feld direkt verfügbar

Der manuelle Ansatz bleibt sinnvoll, wenn kein React 19 verfügbar ist oder wenn Formulare ohne Server Actions mit fetch arbeiten. In diesen Fällen kann useFormStatus nicht eingesetzt werden, da der Pending-Zustand nur bei nativen Formular-Submissions und Server Actions gesetzt wird. Für reine Client-seitige Formulare mit onSubmit und fetch ist manueller State nach wie vor die richtige Wahl.

8. Einschränkungen und typische Fehler

Die wichtigste Einschränkung von useFormStatus ist die Eltern-Kind-Bedingung. Der Hook muss in einer Komponente aufgerufen werden, die ein Kind-Element des <form>-Elements ist – nicht in der Komponente, die das <form>-Element selbst rendert. Diese Bedingung ist intuitiv, aber leicht zu vergessen. Das Symptom: pending ist immer false, egal wie lange die Action dauert. Die Lösung ist immer, den SubmitButton in eine eigene Komponente auszulagern.

Eine zweite Einschränkung: useFormStatus ist ein react-dom-Hook und funktioniert nur in Browser-Umgebungen. In Server-Komponenten kann er nicht aufgerufen werden. Die Komponente, die useFormStatus verwendet, muss deshalb mit der 'use client'-Direktive markiert sein. Das ist in der Regel kein Problem, da Submit-Buttons per Definition interaktiv und damit Client-Komponenten sind.

Ein dritter häufiger Fehler: Das Lesen von data ohne Null-Check. Das data-Feld ist null, wenn kein Pending-Zustand besteht. Wer direkt data.get('field') aufruft, ohne vorher if (!data) zu prüfen, erhält einen Runtime-Fehler. Immer mit if (!pending || !data) return null beginnen, bevor auf data zugegriffen wird.


'use client';

import { useFormStatus } from 'react-dom';

// WRONG: useFormStatus in the same component as <form> — pending always false
function WrongExample() {
  const { pending } = useFormStatus(); // Always false here!

  return (
    <form action={someAction}>
      <button disabled={pending}>Speichern</button>
    </form>
  );
}

// RIGHT: SubmitButton is a separate child component
function SubmitButton() {
  const { pending } = useFormStatus(); // Reads from parent <form>
  return <button type="submit" disabled={pending}>Speichern</button>;
}

function RightExample() {
  return (
    <form action={someAction}>
      <SubmitButton /> {/* Child of <form> — works correctly */}
    </form>
  );
}

// RIGHT: null-check before accessing data
function OptimisticPreview() {
  const { pending, data } = useFormStatus();

  // Always guard against null before accessing data
  if (!pending || !data) return null;

  const title = data.get('title') as string | null;
  if (!title) return null;

  return <p>Wird gespeichert: <strong>{title}</strong></p>;
}

// RIGHT: 'use client' directive is required
// This file must be a Client Component because useFormStatus is a DOM hook

9. TypeScript-Typisierung und Best Practices

useFormStatus ist vollständig in den @types/react-dom-Definitionen erfasst. Der Rückgabetyp ist { pending: boolean; data: FormData | null; method: string | null; action: string | ((formData: FormData) => void | Promise) | null }. In TypeScript-Projekten empfiehlt es sich, den Rückgabetyp nicht manuell zu annotieren – der Hook ist ausreichend typisiert, und TypeScript leitet die Typen korrekt ab.

Für wiederverwendbare Submit-Button-Komponenten empfiehlt sich die Definition klarer Props. Der Button sollte eine label-Prop für den Standard-Text und eine pendingLabel-Prop für den Pending-Text akzeptieren. Das macht die Komponente flexibel genug für verschiedene Formulare, ohne die Pending-Logik an jedem Verwendungsort zu duplizieren. Icon-Slots via ReactNode-Props ermöglichen es, verschiedene Spinner oder Icons einzusetzen, ohne die Komponente zu verzweigen.

Eine häufig übersehene Best Practice: Der Submit-Button sollte nicht nur disabled sein, sondern auch type="submit" explizit setzen. Wenn ein Formular mehrere Buttons enthält und keiner explizit type="submit" hat, sendet der Browser das Formular beim Drücken von Enter über den ersten Button ab – was zu unerwarteten Verhaltensweisen führt. Außerdem sollte jede Komponente, die useFormStatus verwendet, die 'use client'-Direktive tragen, auch wenn das gesamte Formular ansonsten mit Server Components arbeitet.

10. Zusammenfassung

useFormStatus löst ein real existierendes Problem in React-Formular-Architektur: den Pending-Status ohne Prop-Drilling in Kind-Komponenten verfügbar zu machen. Der Hook ist maximal einfach – ein Aufruf, vier Felder – und integriert sich nahtlos in das neue Server-Action-Ökosystem von React 19. Submit-Buttons, Lade-Indikatoren und optimistische Previews lassen sich damit als komplett unabhängige, wiederverwendbare Client-Komponenten implementieren.

Die kritischen Punkte für die korrekte Verwendung: Die Komponente mit dem Hook muss ein Kind des Formular-Elements sein, nicht in derselben Komponente stehen. Sie muss mit 'use client' markiert sein. Das data-Feld muss vor dem Zugriff auf Null geprüft werden. Und für rein Client-seitige Formulare mit fetch und onSubmit bleibt manueller State die richtige Wahl – useFormStatus reagiert nur auf native Formular-Submissions und Server Actions.

useFormStatus — Das Wichtigste auf einen Blick

Kind-Komponente Pflicht

useFormStatus muss in einer Kind-Komponente des Formulars aufgerufen werden. In derselben Komponente wie das form-Element gibt pending immer false zurück.

use client erforderlich

useFormStatus ist ein react-dom-Hook und funktioniert nur in Client-Komponenten. Die Datei muss mit 'use client' markiert sein.

data-Null-Check

data ist null wenn kein Pending-Zustand besteht. Immer mit if (!pending || !data) return null absichern vor dem Zugriff auf Formular-Felder.

Server Actions Integration

Nahtlose Integration mit Server Actions – pending wird automatisch während der Server-Funktion gesetzt ohne manuelle State-Verwaltung.

11. FAQ: React useFormStatus

1Was macht useFormStatus?
Gibt Kind-Komponenten Zugriff auf pending, data, method und action des Parent-Formulars – ohne Prop-Drilling und ohne externen State.
2Warum Kind-Komponente Pflicht?
Der Hook liest den Status des Parent-Formulars. In derselben Komponente wie form gibt es keinen Parent – pending wäre immer false.
3Ohne Server Actions nutzbar?
Nein. Reagiert nur auf native Formular-Submissions und Server Actions. Für fetch/onSubmit-Formulare manuellen State verwenden.
4data-Feld: Null-Check nötig?
Ja. data ist null außerhalb des Pending-Zustands. Immer if (!pending || !data) return null prüfen, bevor data.get() aufgerufen wird.
5'use client' erforderlich?
Ja. useFormStatus ist ein react-dom-Hook – funktioniert nur im Browser. Datei muss mit 'use client' markiert sein.
6Für optimistische UI geeignet?
Für einfache Fälle ja – data enthält die übermittelten Werte. Für komplexe optimistische Updates mit Rollback useOptimistic nutzen.
7Unterschied zu useActionState?
useFormStatus: Status während laufender Aktion. useActionState: Ergebnis-State nach abgeschlossener Aktion und Action-Funktion für das form-action-Attribut.
8Zugänglicher Pending-Button?
disabled und aria-disabled setzen. Button-Text von "Speichern" auf "Wird gesendet …" ändern – nur ein Spinner-Icon ohne Textänderung ist nicht ausreichend zugänglich.
9Mehrere Kind-Komponenten gleichzeitig?
Ja. Beliebig viele Kind-Komponenten können useFormStatus aufrufen und lesen alle denselben Pending-Status des Parent-Formulars.
10Ab welcher React-Version?
Stabil seit React 19. In React 18 experimentell unter react-dom/experimental verfügbar.