Server Actions & Formulare
Schreiben ohne API dazwischen
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.
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>
);
} 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.
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
"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);
} 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.
"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>
);
} "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 };
} 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
"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>
);
} - +
Formulare aller Art – anlegen, ändern, löschen
- +
Einzelaktionen wie Favorit setzen oder abonnieren
- +
Alles, wonach du ohnehin revalidieren willst
- −
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.
"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>;
} 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
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 FehlertextBitte 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> ); }