11. Formulare mit TanStack Form
Formulare mit TanStack Form
Abschnitt betitelt „Formulare mit TanStack Form“Formulare sind die Stelle, an der Benutzer Daten in Ihr System bringen. Sie sind damit gleichzeitig die Stelle, an der Anwendungen am schnellsten unangenehm werden: eine Fehlermeldung, die nicht sagt, welches Feld gemeint ist; ein Absenden, das nichts sichtbar tut; eine Eingabe, die nach dem Fehler gelöscht ist; eine Regel, die im Formular anders lautet als auf dem Server.
Das Ziel dieses Kapitels ist ein Formularmodell, in dem jede Regel genau einmal beschrieben und trotzdem auf beiden Seiten durchgesetzt wird.
Warum Formulare schwierig sind
Abschnitt betitelt „Warum Formulare schwierig sind“Ein Eingabefeld hat mehr Zustand, als man beim Hinsehen vermutet: den aktuellen Wert, die Information, ob es schon einmal berührt wurde, ob es seit dem Laden verändert wurde, ob gerade eine Prüfung läuft und welche Fehler anliegen. Das Formular als Ganzes hat zusätzlich einen Absendezustand und mögliche Fehler, die erst der Server kennt.
Das Muster aus SEW, ein useState pro Feld, trägt bis etwa fünf Feldern. Danach häufen sich drei Probleme: Bei fünfzehn Feldern stehen fünfzehn Zustandsvariablen samt Fehlerbehandlung im Code; Abhängigkeiten zwischen Feldern (das Ende muss nach dem Beginn liegen) verteilen sich über mehrere Stellen; und jede Tasteneingabe zeichnet das gesamte Formular neu, was bei größeren Masken spürbar wird.
Eine Formularbibliothek löst genau diese drei Punkte: Sie hält den Zustand pro Feld, kennt Querbedingungen und zeichnet nur das neu, was sich geändert hat.
Kontrolliert oder unkontrolliert
Abschnitt betitelt „Kontrolliert oder unkontrolliert“Dahinter steht eine Entscheidung, die man einmal verstanden haben sollte, weil sie erklärt, warum sich die Bibliotheken unterscheiden.
Bei einem kontrollierten Feld ist React die Wahrheit: Der Wert steht im Zustand, value kommt von dort, und jede Tasteneingabe löst eine Zustandsänderung und damit eine Neuzeichnung aus. Der Vorteil ist volle Kontrolle, etwa um Eingaben während des Tippens zu formatieren oder Felder voneinander abhängig zu machen. Der Preis ist eine Neuzeichnung pro Anschlag.
Bei einem unkontrollierten Feld ist das DOM die Wahrheit: Der Browser hält den Wert, React liest ihn erst, wenn er gebraucht wird. Das ist schneller und weniger Code, dafür weiß die Anwendung zwischendurch nicht, was drinsteht.
Die Frage ist selten „was ist besser”, sondern „wie klein bleibt der neu gezeichnete Bereich”. TanStack Form arbeitet kontrolliert, hält die Neuzeichnung aber auf das einzelne Feld begrenzt, statt das ganze Formular zu erfassen. React Hook Form geht den anderen Weg und arbeitet überwiegend unkontrolliert. Bei einem Formular mit acht Feldern merkt man den Unterschied nicht; bei einer Tabelle mit dreißig editierbaren Zeilen sehr wohl.
Ein Schema, zwei Seiten
Abschnitt betitelt „Ein Schema, zwei Seiten“In Kapitel 7 haben Sie das Schema für die Eingabe geschrieben. Dieses Schema wird jetzt nicht abgeschrieben, sondern importiert.
// src/schemas/booking.ts - used by server and client alikeexport const CreateBookingSchema = z.object({ machineId: z.number().int().positive(), start: z.coerce.date(), end: z.coerce.date(), note: z.string().max(500).optional(),}).refine((d) => d.end > d.start, { message: "The end must be after the start.", path: ["end"],});Damit ist die Frage beantwortet, die sonst nach drei Wochen zu unterschiedlichen Regeln führt: Es gibt keine zweite Stelle, an der „höchstens 500 Zeichen” steht.
Was daraus nicht folgt, ist eine Aufhebung der Serverprüfung. Die Arbeitsteilung lautet:
Der Client validiert für die Benutzerführung. Er sagt sofort und am richtigen Feld, was nicht passt, damit niemand ein Formular absendet, das ohnehin zurückkommt.
Der Server validiert für die Wahrheit. Er nimmt an, dass die Eingabe von einem curl-Aufruf stammt, denn genau das kann sie.
Dazu kommt eine dritte Art von Regel, die der Client gar nicht prüfen kann, weil ihm die Information fehlt: Ist dieser Benutzername noch frei? Ist das Zeitfenster inzwischen belegt? Hat das Mitglied noch Kontingent? Solche Regeln werden im Formular asynchron geprüft, damit die Rückmeldung früh kommt, und beim Absenden auf dem Server noch einmal, weil zwischen Prüfung und Absenden Zeit vergeht. Der 409 aus Kapitel 7 ist genau dieser Fall, und die letzte Instanz ist die Datenbank mit ihrem Eindeutigkeitsindex aus Kapitel 6.
TanStack Form
Abschnitt betitelt „TanStack Form“const form = useForm({ defaultValues: { machineId, start: "", end: "", note: "" }, validators: { onChange: CreateBookingSchema, // while typing onSubmit: CreateBookingSchema, // at the latest on submit }, onSubmit: async ({ value }) => { await createBooking.mutateAsync(value); },});Der Zeitpunkt der Prüfung ist eine Entscheidung über die Bedienbarkeit. Bei onChange erscheint der Fehler, während getippt wird, was bei einer E-Mail-Adresse bedeutet, dass sie bis zum letzten Zeichen als ungültig markiert ist. Die verbreitete Kombination ist deshalb: erste Prüfung beim Verlassen des Feldes, danach bei jeder Änderung, und in jedem Fall beim Absenden.
Ein Feld sieht damit so aus:
<form.Field name="end"> {(field) => ( <div> <label htmlFor={field.name}>End</label> <input id={field.name} type="datetime-local" value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} aria-invalid={field.state.meta.errors.length > 0} aria-describedby={`${field.name}-error`} /> <p id={`${field.name}-error`} role="alert"> {field.state.meta.errors.join(", ")} </p> </div> )}</form.Field>Asynchrone Prüfungen
Abschnitt betitelt „Asynchrone Prüfungen“<form.Field name="username" validators={{ onChangeAsyncDebounceMs: 400, onChangeAsync: async ({ value }) => (await api.isUsernameAvailable(value)) ? undefined : "This name is already taken.", }}>Die Entprellung ist kein Detail: Ohne sie schickt jedes getippte Zeichen eine Anfrage, und Ihre Ratenbegrenzung aus Kapitel 7 antwortet nach dem achten Buchstaben mit 429.
Absenden
Abschnitt betitelt „Absenden“<button type="submit" disabled={!form.state.canSubmit || form.state.isSubmitting}> {form.state.isSubmitting ? "Saving ..." : "Book"}</button>Die gesperrte Schaltfläche verhindert den Doppelklick. Sie ist Bedienkomfort und kein Schutz: Ein zweites Absenden über die Entwicklerwerkzeuge oder ein Netzabbruch mit Wiederholung erzeugen weiterhin zwei Datensätze. Der Schutz liegt im Idempotenzschlüssel aus Kapitel 7. Beides zusammen ergibt eine Oberfläche, die sich richtig anfühlt, und einen Server, der richtig ist.
Serverfehler an das richtige Feld
Abschnitt betitelt „Serverfehler an das richtige Feld“Das Fehlerformat aus Kapitel 7 hat ein Feld errors mit den Feldnamen als Schlüssel. Genau dafür war es da:
onSubmit: async ({ value, formApi }) => { try { await createBooking.mutateAsync(value); } catch (error) { if (error.status === 409 || error.status === 400) { for (const [field, message] of Object.entries(error.errors ?? {})) { formApi.setFieldMeta(field, (m) => ({ ...m, errors: [message] })); } return; } throw error; // unexpected: on to the error boundary }}Ohne diesen Schritt landet „Zeitfenster belegt” als allgemeine Meldung über dem Formular, und der Benutzer sucht selbst, welches Feld gemeint ist.
Fortgeschrittene Formen
Abschnitt betitelt „Fortgeschrittene Formen“Dynamische Feldlisten entstehen überall dort, wo eine unbekannte Anzahl von Einträgen erfasst wird: Teilnehmer einer Anmeldung, Positionen einer Bestellung, Zeitfenster einer Serienbuchung. Das Schema beschreibt das als Array, und das Formular bietet Hinzufügen, Entfernen und Umsortieren an. Wichtig ist, dass Fehler am richtigen Eintrag erscheinen und nicht an der Liste.
Mehrstufige Formulare teilen eine lange Maske in Abschnitte. Drei Punkte entscheiden über die Brauchbarkeit: Jeder Schritt wird beim Weitergehen geprüft, aber der ganze Datensatz erst am Ende abgesendet. Der Zwischenstand überlebt ein versehentliches Neuladen, entweder lokal im Browser oder als Entwurf auf dem Server. Und der Rückweg zu einem früheren Schritt verliert keine Eingaben.
Datei-Uploads brauchen im Formular einen Fortschrittsbalken, eine Vorschau und eine Prüfung von Typ und Größe, damit niemand 40 MB hochlädt und danach eine Fehlermeldung bekommt. Diese Prüfung ersetzt die serverseitige nicht: Die Regeln aus Kapitel 9 gelten unverändert, also Typ am Inhalt prüfen, eigener Dateiname, Ablage außerhalb des ausgelieferten Verzeichnisses.
Ungespeicherte Änderungen sollten nicht wortlos verlorengehen. Wer ein verändertes Formular verlässt, bekommt eine Rückfrage. Im Browser übernimmt das beforeunload, innerhalb der Anwendung braucht es eine Abfrage im Router.
Formulare und Barrierefreiheit
Abschnitt betitelt „Formulare und Barrierefreiheit“Ein Formular ist die Stelle, an der schlechte Auszeichnung sofort jemanden ausschließt. Sechs Punkte, die zusammen den größten Teil ausmachen:
Jedes Feld hat eine echte Beschriftung über label mit htmlFor. Ein Platzhalter ist keine Beschriftung: Er verschwindet beim Tippen, wird von manchen Screenreadern nicht vorgelesen und hat oft zu wenig Kontrast.
Fehler sind mit dem Feld verknüpft über aria-describedby, und das Feld ist mit aria-invalid markiert. Sonst hört eine blinde Person „Ende, Eingabefeld” und erfährt nichts über den Fehler darunter.
Fehler werden angesagt. Ein Container mit role="alert" oder aria-live="polite" sorgt dafür, dass eine neu erscheinende Meldung vorgelesen wird.
Der Fokus springt zum ersten Fehler, wenn ein Absenden fehlschlägt. Bei langen Formularen kommt zusätzlich eine Fehlerzusammenfassung an den Anfang, mit Sprungmarken zu den betroffenen Feldern.
Eingabetypen und autocomplete sind gesetzt. type="email", inputmode="numeric", autocomplete="street-address": Das blendet auf Mobilgeräten die passende Tastatur ein und erlaubt dem Browser, korrekt auszufüllen.
Fehlermeldungen sagen, was zu tun ist. „Ungültige Eingabe” hilft niemandem. „Das Datum liegt in der Vergangenheit. Wählen Sie einen Termin ab morgen.” schon.
Häufige Stolperfallen
Abschnitt betitelt „Häufige Stolperfallen“- Validierungsregeln doppelt geschrieben. Nach drei Wochen unterscheiden sie sich, und niemand weiß, welche gilt.
- Nur der Client validiert. Ein Aufruf mit
curlumgeht alles, und die Datenbank bekommt, was sie nicht will. - Fehler als Sammelmeldung oben statt am betroffenen Feld. Der Benutzer sucht selbst.
- Serverfehler nicht zugeordnet. Das Fehlerformat aus Kapitel 7 liefert die Feldnamen, sie werden nur nicht verwendet.
- Absenden-Schaltfläche nicht gesperrt und kein Idempotenzschlüssel. Zwei Datensätze aus einem Doppelklick.
- Prüfung ohne Entprellung bei jeder Tasteneingabe, bis die Ratenbegrenzung greift.
- Upload nur über die Dateiendung geprüft.
bild.jpg.phpbesteht diese Prüfung. - Platzhalter statt Beschriftung. Für Screenreader und für alle, die den Text beim Ausfüllen noch brauchen, unbrauchbar.
- Eingaben nach einem Fehler verworfen. Der schnellste Weg, jemanden zu verlieren, der ein langes Formular ausgefüllt hat.
Lernergebnisse
Abschnitt betitelt „Lernergebnisse“- Erklären: benennen, welchen Zustand ein Formularfeld hat, und begründen, warum ein Zustandshaken pro Feld nicht skaliert.
- Anwenden: ein Formular mit Feld- und Formularvalidierung aus dem geteilten Schema der Schnittstelle umsetzen.
- Beurteilen: entscheiden, welche Prüfung in den Client, welche in den Server und welche in die Datenbank gehört.
- Anwenden: Validierungszeitpunkte begründet wählen und ihre Wirkung auf die Bedienbarkeit beschreiben.
- Anwenden: asynchrone Prüfungen mit Entprellung einbauen.
- Anwenden: Serverfehler aus problem+json den richtigen Feldern zuordnen.
- Erklären: erklären, warum eine gesperrte Schaltfläche kein Ersatz für Idempotenz ist.
- Erschaffen: ein mehrstufiges Formular mit dynamischen Feldern, Zwischenspeicherung und Upload bauen.
- Anwenden: Formulare barrierefrei auszeichnen und mit
aria-invalid,aria-describedbyund Fokusführung nutzbar machen. - Analysieren: ein Formular allein mit der Tastatur bedienen und die gefundenen Befunde beheben.
Arbeit am Jahresprojekt
Abschnitt betitelt „Arbeit am Jahresprojekt“- Alle schreibenden Masken verwenden dasselbe Schema wie die Schnittstelle, importiert und nicht kopiert.
- Serverfehler erscheinen am betroffenen Feld, nachgewiesen an einem 409-Fall aus dem eigenen Projekt.
- Mindestens ein Formular hat eine asynchrone Prüfung mit Entprellung.
- Mindestens ein Formular ist mehrstufig oder hat eine dynamische Feldliste.
- Alle Formulare sind vollständig mit der Tastatur bedienbar, Felder haben Beschriftungen, Fehler sind verknüpft und werden angesagt.
- Ein Upload (falls vorhanden) prüft Typ und Größe im Client und erneut auf dem Server.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe - Ein Schema, zwei Seiten
- Aufgabe - Asynchrone Verfügbarkeitsprüfung
- Aufgabe - Mehrstufiges Formular mit Wiederaufnahme
- Aufgabe - Formular für Screenreader nachbessern