9.3 Next.js: Daten verändern, Formulare und Mutationen
Nach dieser Einheit kannst du:
- eine Mutation aus einem Formular umsetzen: Absenden per
fetch, Schutz gegen Doppelklick, Lade- und Fehleranzeige. - die Meldung aus dem Fehlerformat des Vertrags anzeigen und einen Netzwerkfehler davon unterscheiden.
- nach einer Mutation die Anzeige aktualisieren und
router.refresh()vonrouter.push()abgrenzen. - Server Actions einordnen und begründen, warum der Kurs den Weg über die eigene API nimmt.
Du tippst auf “Bestellen”. Zwei Sekunden lang passiert nichts. War der Klick zu kurz? Du tippst noch einmal. Am Buffet liegen jetzt zwei Schinkenweckerl für dich bereit, und beide sind zu bezahlen. Die Oberfläche hat dir drei Dinge verschwiegen: dass die Bestellung unterwegs ist, ob sie angekommen ist, und was schiefging, falls nicht.
Am Schalter läuft das besser. Du gibst den Zettel ab und wartest sichtbar in der Schlange, niemand gibt denselben Zettel zweimal ab. Dann bekommst du entweder deine Abholnummer oder die Auskunft “Weckerl sind aus”. Genau diese drei Zustände, unterwegs, gelungen, gescheitert, muss auch jedes Formular zeigen, das Daten am Server verändert.
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- Mutation: eine Operation, die Daten am Server verändert, also anlegen, ändern oder löschen (
POST,PATCH,DELETE). - In-Flight-Flag: ein Merker, der anzeigt, dass eine Anfrage noch unterwegs ist (Einheit 4.2).
- Controlled Component: ein Formularfeld, dessen Wert im State liegt (Einheit 7.5).
- Server Action: eine Funktion, die am Server läuft und direkt aus einem Formular aufgerufen wird.
Formular und fetch: die Mutation Schritt für Schritt
Abschnitt betitelt „Formular und fetch: die Mutation Schritt für Schritt“Schreiboperationen laufen vom Formular über fetch an die eigene API aus Einheit 9.1. Das Formular ist ein Controlled Component nach dem Muster aus Einheit 7.5; neu ist der Umgang mit dem Ergebnis:
"use client";
import { useRouter } from "next/navigation";import { useState } from "react";
export function NewJobForm() { const router = useRouter(); const [filename, setFilename] = useState(""); const [error, setError] = useState<string | null>(null); const [submitting, setSubmitting] = useState(false);
async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (submitting) return; // no double submits setSubmitting(true); setError(null); try { const response = await fetch("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename, preset: "web-1080p" }), }); if (!response.ok) { const problem = await response.json(); setError(problem.error.message); // the contract's error format return; } setFilename(""); router.refresh(); // re-render server components } catch { setError("network error, please try again"); } finally { setSubmitting(false); } }
return ( <form onSubmit={handleSubmit}> <input value={filename} onChange={(e) => setFilename(e.target.value)} /> <button type="submit" disabled={submitting || filename.trim() === ""}> {submitting ? "creating ..." : "create job"} </button> {error && <p role="alert">{error}</p>} </form> );}Beispielprojekt: 09_nextjs_api_datenfluss_examples/ex04_mutations
Hier laufen die Fäden des Halbjahres zusammen:
- Unterwegs: Das In-Flight-Flag
submittingdeaktiviert den Knopf und ändert seinen Text. Ein zweiter Klick trifft einen gesperrten Knopf; das ist die Abwehr der Doppelauslösung aus Einheit 4.2. - Gescheitert, der Server lehnt ab:
response.okist falsch. Die Meldung für den Menschen steht im Fehlerformat aus Einheit 6.2 untererror.message; das Formular erfindet keine eigene. - Gescheitert, das Netz fällt aus:
fetchselbst wirft, es gibt gar keine Antwort. Diesen Fall fängtcatch. Das sind die Fehlerfälle an der Netzwerkgrenze aus Einheit 5.2. - Gelungen: Das Feld wird geleert, und
router.refresh()lässt die Server-Komponenten der Seite neu rendern, damit die neue Zeile in der Liste erscheint. finallygibt den Knopf in jedem Fall wieder frei.
flowchart LR
accTitle: Die drei Zustände einer Mutation in der Oberfläche
idle["bereit:<br/>Knopf aktiv"] -- "Absenden" --> pending["unterwegs:<br/>Knopf gesperrt,<br/>creating ..."]
pending -- "2xx" --> ok["gelungen:<br/>Feld leeren,<br/>Liste aktualisieren"]
pending -- "4xx, 5xx<br/>oder kein Netz" --> fail["gescheitert:<br/>Meldung anzeigen"]
ok --> idle
fail --> idle
Abb. 9.7: Jede Mutation durchläuft “unterwegs” und endet sichtbar in “gelungen” oder “gescheitert” (eigene Darstellung).
Löschen und Ändern folgen demselben Muster mit DELETE und PATCH.
Der Knopf ist auch gesperrt, solange das Feld leer ist. Diese Prüfung im Browser ist Komfort: Sie erspart eine sinnlose Anfrage. Die Prüfung im Route Handler bleibt Pflicht, denn an der API kommt nicht nur dein Formular an, sondern auch curl, ein Skript oder ein manipulierter Client. Verzichtbar wäre also nur die Prüfung im Browser, nie die am Server.
Wo begegnet dir das? Beim Bezahlen im Online-Shop: Der Knopf “Jetzt kaufen” wird nach dem Klick grau und zeigt “Wird verarbeitet …”. Das ist kein Schmuck, sondern das In-Flight-Flag, das eine doppelte Abbuchung verhindert.
Auflösung
(a) “filename must not be empty”: response.ok ist falsch, setError(problem.error.message) zeigt die Meldung des Vertrags. (b) “network error, please try again”: fetch wirft, der catch-Zweig setzt die Meldung. (c) Die Fehlermeldung erscheint, aber der Knopf zeigt für immer “creating …” und bleibt gesperrt: Ohne finally setzt niemand submitting zurück.
Das router-Objekt
Abschnitt betitelt „Das router-Objekt“router stammt aus dem Hook useRouter() aus next/navigation. Er ist das Gegenstück zur Komponente Link aus Einheit 8.1: Was <Link> für einen Klick erledigt, macht das router-Objekt aus dem Code heraus, also dann, wenn erst nach einer Aktion feststeht, wohin oder ob navigiert wird, etwa nach einem gelungenen fetch. useRouter() gibt es nur in Client-Komponenten; die Datei braucht "use client".
| Methode | Wirkung |
|---|---|
router.push("/jobs/42") | Navigiert zur Adresse und legt einen neuen Eintrag im Verlauf an; “Zurück” führt zurück. Typisch nach dem Anlegen: weiter zur Detailseite. |
router.replace("/login") | Wie push, ersetzt aber den aktuellen Eintrag im Verlauf. Für Weiterleitungen, zu denen man nicht “zurück” können soll. |
router.back(), router.forward() | Ein Schritt im Verlauf zurück oder vor, wie die Pfeile des Browsers. |
router.refresh() | Rendert die Server-Komponenten der aktuellen Seite neu und übernimmt das Ergebnis, ohne den Zustand im Browser (Formularinhalte, Scrollposition) zu verlieren. |
Wichtig ist die Abgrenzung: push und replace wechseln die Seite, refresh() bleibt auf derselben Seite und holt nur deren Serverdaten neu. Nach dem Anlegen eines Jobs auf der Listenseite ist refresh() richtig; soll die Anwendung zur Detailseite des neuen Jobs springen, ist es push.
sequenceDiagram
accTitle: Ablauf von Absenden bis zur aktualisierten Liste
participant f as NewJobForm im Browser
participant a as POST /api/jobs
participant p as Seite /jobs am Server
f->>a: fetch mit filename
a-->>f: 201 mit dem neuen Job
f->>p: router.refresh()
p-->>f: Liste neu gerendert, Formular-State bleibt
Abb. 9.8: Erst die Mutation über die API, dann holt router.refresh() die neue Liste von der Server-Komponente (eigene Darstellung).
Neu gerendert wird dabei die Liste hinter connection() und <Suspense> aus Einheit 9.2; der Formular-State im Browser bleibt, wie er ist.
Server Actions: der andere Weg für Mutationen
Abschnitt betitelt „Server Actions: der andere Weg für Mutationen“Für Mutationen bietet Next.js neben dem ausdrücklichen fetch gegen die eigene API einen zweiten Weg: Server Actions. Das ist eine Funktion mit der Direktive "use server", die am Server läuft und direkt aus einer Komponente aufgerufen wird. Ein Formular ruft sie über sein Attribut action auf, ohne sichtbares fetch, ohne Route Handler und ohne selbst geschriebenen Code für die Anfrage:
"use server";
import { revalidatePath } from "next/cache";import { addJob } from "@/lib/jobs";
export async function createJob(formData: FormData) { const filename = String(formData.get("filename")); addJob({ filename, preset: "web-1080p" }); revalidatePath("/jobs-action"); // re-render the list with fresh data}// app/jobs-action/new-job-form.tsx: the form calls the action directlyimport { createJob } from "./actions";
export function NewJobForm() { return ( <form action={createJob}> <input name="filename" /> <button type="submit">Add</button> </form> );}Beispielprojekt: 09_nextjs_api_datenfluss_examples/ex04_mutations
Das ist auffällig kürzer als der Weg weiter oben: kein fetch, keine Prüfung des Statuscodes, kein router.refresh(). Next.js verdrahtet Formular, Serveraufruf und Neuaufbau der Seite selbst und leert das Feld nach dem Absenden. Diese Bequemlichkeit hat eine Kehrseite.
flowchart TB
accTitle: Mutation über die eigene API und über eine Server Action
subgraph api["Weg über die API"]
direction LR
f1["Formular"] -- "fetch POST /api/jobs" --> h1["Route Handler:<br/>Vertrag mit Status<br/>und Fehlerformat"]
c1["curl, Skript,<br/>Handy-App"] -- "dieselbe Anfrage" --> h1
end
subgraph act["Weg über die Server Action"]
direction LR
f2["Formular"] -- "action=createJob" --> h2["Funktion mit use server:<br/>nur aus dieser<br/>Anwendung erreichbar"]
end
Abb. 9.9: Die API steht jedem Client offen, die Server Action nur dem eigenen Formular (eigene Darstellung).
Warum wir in diesem Jahr beim Weg über die API bleiben. Eine Server Action versteckt genau das, was dieses Jahr lehren soll: den HTTP-Vertrag. Beim Weg über die eigene API entsteht ein POST /api/jobs mit dokumentiertem Body, festgelegten Statuscodes und einem Fehlerformat. Diese Schnittstelle kann jeder Client nutzen: das eigene Formular, ein Skript, eine Handy-App, ein fremdes System. Eine Server Action ist an Next.js gebunden, steht in keiner API-Dokumentation und ist von außen nicht wie eine REST-Ressource ansprechbar. Wer zuerst den ausdrücklichen Weg beherrscht, versteht, was eine Server Action im Hintergrund abkürzt; umgekehrt nicht.
Beachte auch, was im Beispiel fehlt: Die Action prüft den Dateinamen nicht und meldet keinen Fehler. Für eine echte Anwendung bräuchte sie dieselbe Sorgfalt wie der Route Handler.
Auflösung
(a) router.refresh(): dieselbe Seite, frische Serverdaten. (b) router.push("/orders/41"): ein Seitenwechsel, “Zurück” führt wieder zum Formular. (c) Keines: Die Mutation ist gescheitert, die Daten sind unverändert. Die Oberfläche zeigt die Meldung aus error.message und bleibt, wo sie ist.
Stolperfallen
Abschnitt betitelt „Stolperfallen“- Kein In-Flight-Flag: Ein Doppelklick schickt zwei Anfragen, es entstehen zwei Jobs oder zwei Bestellungen.
response.oknicht geprüft: Eine400-Antwort wird wie ein Erfolg behandelt; das Feld wird geleert, obwohl nichts gespeichert wurde.- Eigene Fehlermeldung erfunden: Das Formular zeigt “Fehler!” statt
error.message; der Mensch erfährt nicht, was zu korrigieren ist. finallyvergessen: Nach dem ersten Fehler bleibt der Knopf gesperrt.router.refresh()vergessen: Die Mutation gelingt, die Liste zeigt den alten Stand bis zum Neuladen.useRouteraus dem falschen Paket: Im App Router kommt der Hook ausnext/navigation, nicht ausnext/router.- Nur im Browser geprüft: Der gesperrte Knopf hält curl nicht auf; ohne Prüfung im Route Handler landen leere Einträge im Speicher.
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- Next.js: useRouter - alle Methoden des
router-Objekts. - Next.js: Mutating Data - Server Actions, Formulare und das Aktualisieren der Anzeige.
- React: form - das Attribut
actionmit einer Funktion. - MDN: Idempotent - warum ein wiederholtes
POSTheikel ist und ein wiederholtesPUTnicht (englisch).
Zum Anhören
Abschnitt betitelt „Zum Anhören“Du kannst dir diese Einheit kostenlos mit der Vorlesefunktion deines Browsers oder Betriebssystems vorlesen lassen (etwa „Laut vorlesen” in Edge), ohne Konto und ohne Upload; die Codebeispiele und Diagramme liest du dabei am Bildschirm mit. Freiwillig kannst du mit Google NotebookLM (Google-Konto nötig) aus diesem öffentlichen Text eine gesprochene Zusammenfassung erzeugen, auch auf Deutsch.
Lernergebnisse
Abschnitt betitelt „Lernergebnisse“Du kannst jetzt:
- eine Mutation aus einem Formular umsetzen: Absenden per
fetch, Schutz gegen Doppelklick, Lade- und Fehleranzeige. - die Meldung aus dem Fehlerformat des Vertrags anzeigen und einen Netzwerkfehler davon unterscheiden.
- nach einer Mutation die Anzeige aktualisieren und
router.refresh()vonrouter.push()abgrenzen. - Server Actions einordnen und begründen, warum der Kurs den Weg über die eigene API nimmt.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 09-2a - Client trifft Server
Quellen
Abschnitt betitelt „Quellen“useRouter,push,replace,back,forward,refresh→ Next.js: useRouter- Server Actions,
"use server",revalidatePath→ Next.js: Mutating Data, revalidatePath; Formular mitactionals Funktion, Zurücksetzen des Formulars → React: form - Verhalten des Formulars (gesperrter Knopf beim zweiten Klick, Meldung aus
error.message, neue Zeile nachrouter.refresh()), der Server Action (neue Zeile nachrevalidatePath, geleertes Feld) und der Build-Bericht → eigener Test mit Next.js 16.4.0, Cache Components und Chrome (Oktober 2026) - In-Flight-Flag → Einheit 4.2; Fehlerfälle von
fetch→ Einheit 5.2; Fehlerformat → Einheit 6.2 - Abb. 9.7 (Zustände einer Mutation), Abb. 9.8 (Absenden und
router.refresh()), Abb. 9.9 (API und Server Action) → eigene Darstellung, alle als Mermaid-Diagramm
Autoren
Abschnitt betitelt „Autoren“- Oliver Maurhart (MAO)
- Michael Gabriel (GAM)
CC-BY-SA