Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Zur Übersicht
Kern 15 Min Lesezeit

Server Actions & Formulare

Schreiben ohne API dazwischen

In einem Satz

Eine Server Action ist eine Funktion, die auf dem Server läuft und die du direkt aus einem Formular aufrufst. Kein fetch, keine Route, kein JSON.

Daten lesen war einfach: await in der Komponente. Beim Schreiben brauchte man bisher immer eine API-Route, fetch, JSON hin und zurück. Eine Server ActionEine Funktion, die auf dem Server läuft und die du direkt aus einem Formular oder einem Klick aufrufen kannst – ohne eine API dazwischen. macht daraus einen Funktionsaufruf.

app/kontakt/page.tsx
export default function Kontakt() {
  async function absenden(formData: FormData) {
    "use server";                     // läuft auf dem Server

    const mail = formData.get("mail") as string;
    await db.anfrage.create({ data: { mail } });
  }

  return (
    <form action={absenden}>
      <input name="mail" type="email" required />
      <button>Absenden</button>
    </form>
  );
}
Kein onSubmit, kein preventDefault, kein fetch. Das Formular ruft die Funktion direkt auf.
Gut zu wissen Es funktioniert sogar ohne JavaScript

Weil es ein echtes <form> mit action ist, schickt der Browser es zur Not ganz klassisch ab. Wer eine langsame Verbindung hat oder bei wem das Skript noch nicht geladen ist, kann das Formular trotzdem benutzen.

Bildlich gesprochen

Bisher war Schreiben wie ein Brief an die eigene Firma: adressieren, einwerfen, in der Poststelle wieder öffnen. Eine Server Action ist der Anruf ins Nachbarbüro – die Funktion wird direkt aufgerufen, den Postweg baut Next.js unsichtbar drumherum.

Wo Actions leben

app/aktionen.ts
"use server";        // ganz oben: ALLE Exporte sind Server Actions

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function artikelAnlegen(formData: FormData) {
  const titel = formData.get("titel") as string;
  if (!titel?.trim()) return;

  const artikel = await db.artikel.create({ data: { titel } });

  revalidatePath("/blog");
  redirect("/blog/" + artikel.slug);
}
In einer eigenen Datei sind Actions auch aus Client-Komponenten importierbar.
Achtung Eine Server Action ist eine öffentliche Adresse

Next.js legt für jede Action einen Endpunkt an. Jeder kann sie aufrufen – mit beliebigen Werten. Prüfe deshalb in der Action, ob die Person angemeldet ist und ob sie das darf. Dass der Knopf in der Oberfläche versteckt ist, schützt nichts.

Rückmeldung an den Nutzer

Für Validierungsfehler und Ladezustände gibt es useActionState – ein React-Hook, der den letzten Rückgabewert der Action, die Action selbst und einen Wartezustand liefert.

app/kontakt/Formular.tsx
"use client";

import { useActionState } from "react";
import { anfrageSenden } from "../aktionen";

export function Formular() {
  const [zustand, action, laeuft] = useActionState(anfrageSenden, {
    fehler: null,
  });

  return (
    <form action={action}>
      <input name="mail" type="email" />
      <button disabled={laeuft}>{laeuft ? "Sendet …" : "Absenden"}</button>
      {zustand.fehler && <p role="alert">{zustand.fehler}</p>}
    </form>
  );
}
app/aktionen.ts
"use server";

type Zustand = { fehler: string | null; erfolg?: boolean };

export async function anfrageSenden(
  bisher: Zustand,
  formData: FormData,
): Promise<Zustand> {
  const mail = String(formData.get("mail") ?? "");
  if (!mail.includes("@")) return { fehler: "Bitte eine gültige Adresse" };

  await db.anfrage.create({ data: { mail } });
  return { fehler: null, erfolg: true };
}
Bei useActionState bekommt die Action den bisherigen Zustand als erstes Argument.
Tipp Der Absendeknopf kann sich selbst deaktivieren

useFormStatus() liest den Zustand des umgebenden Formulars. Damit baust du einen <SubmitButton>, der überall funktioniert, ohne dass du laeuft durchreichst. Wichtig: Der Hook muss innerhalb des <form> stehen, nicht in derselben Komponente.

Nicht nur Formulare

knopf.tsx
"use client";
import { useTransition } from "react";
import { favorisieren } from "../aktionen";

export function Herz({ id }: { id: string }) {
  const [laeuft, start] = useTransition();

  return (
    <button onClick={() => start(() => favorisieren(id))} disabled={laeuft}>

    </button>
  );
}
Aus einem Klick heraus wickelt man den Aufruf in startTransition – sonst blockiert die Oberfläche.
Nimm es, wenn …
  • +

    Formulare aller Art – anlegen, ändern, löschen

  • +

    Einzelaktionen wie Favorit setzen oder abonnieren

  • +

    Alles, wonach du ohnehin revalidieren willst

Lass es, wenn …
  • Reines Lesen – dafür holst du die Daten direkt in der Server-Komponente

  • Als allgemeine API für fremde Systeme – dafür gibt es Route Handler

  • Ohne Prüfung von Anmeldung und Berechtigung

Genauer erklärt: sofort anzeigen mit useOptimistic optional

Eine Server Action braucht eine Netzwerkrunde. Bei kleinen Aktionen – ein Herz, ein Häkchen – fühlt sich das träge an. useOptimistic zeigt das Ergebnis sofort und nimmt es zurück, falls es schiefgeht.

optimistisch.tsx
"use client";
import { useOptimistic, useTransition } from "react";

export function Liste({ eintraege }: { eintraege: Eintrag[] }) {
  const [sichtbar, ergaenze] = useOptimistic(
    eintraege,
    (bisher, neu: string) => [...bisher, { text: neu, unterwegs: true }],
  );
  const [, start] = useTransition();

  function senden(formData: FormData) {
    const text = String(formData.get("text"));
    start(async () => {
      ergaenze(text);            // sofort sichtbar
      await eintragAnlegen(text); // Server Action
    });
  }

  return <form action={senden}>…</form>;
}
Stolperstein Nur für Aktionen, die fast immer klappen

Schlägt die Action fehl, verschwindet der optimistische Eintrag wieder. Bei einem Herz ist das verkraftbar, bei einer Zahlung nicht.

Sitzt das schon?

5 Fragen zu dieser Lektion. Falsche Antworten landen in deiner Statistik.

Jetzt selbst schreiben

Formular mit Validierung und Wartezustand

Baue das Anmeldeformular mit useActionState fertig. Die Action ist schon da und simuliert den Server – du verkabelst Formular, Fehlermeldung und Knopfbeschriftung. (In Next.js käme die Action aus einer Datei mit "use server".)

  • Ohne @ erscheint der Fehlertext Bitte eine gültige Adresse
  • Mit gültiger Adresse erscheint Danke!
  • Während des Absendens steht Sendet … auf dem Knopf
import { useActionState } from "react";

type Zustand = { fehler: string | null; erfolg?: boolean };

// In Next.js stünde diese Funktion in einer Datei mit "use server".
async function anmelden(bisher: Zustand, formData: FormData): Promise<Zustand> {
  const mail = String(formData.get("mail") ?? "");
  await new Promise((r) => setTimeout(r, 300));
  if (!mail.includes("@")) return { fehler: "Bitte eine gültige Adresse" };
  return { fehler: null, erfolg: true };
}

export default function App() {
  // TODO: useActionState verkabeln

  return (
    <form style={{ fontFamily: "system-ui", padding: 16 }}>
      <input name="mail" aria-label="mail" />{" "}
      <button>Absenden</button>
    </form>
  );
}

Hirefullstack

Ihr braucht React-Verstärkung im Team?

Wir bauen seit Jahren React- und Next.js-Anwendungen für Kunden in ganz Deutschland – als einzelner Experte, als Verstärkung fürs Bestandsteam oder als komplettes Scrum-Team.

Projekt besprechen →