Zum Inhalt
wannacode

Formulare in einer Komponente

Kurz gesagt: Ein kontrolliertes Feld in React hält seinen Wert im Zustand (value und onChange), und so ist der Zustand die einzige Quelle für das Prüfen und Senden des Formulars.

Problem

Eingetippt, aber leer

Der Kunde füllt die Bestellung aus und drückt auf „Bestellen“. Der Server antwortet, der Name sei erforderlich. Und das Feld „Name“ zeigt den Namen.

React

Tipp einen Namen, eine E-Mail-Adresse und eine Adresse ein, akzeptiere die Bedingungen und drück auf „Bestellen“. Das Feld behält „Maria Ivanova“, aber die Anfrage trägt name: ''. Die E-Mail und die Adresse haben value und onChange, der Name nicht. Der Code schickt den Zustand, und der Zustand weiß nicht, was im Feld steht.

Die Idee

Eine Wahrheit: der Zustand

In der Lektion über das DOM hat das Formular den Text selbst gehalten, und der Code hat ihn beim Absenden gelesen. In einer Komponente gibt es einen zweiten Ort: den Zustand. Sind die beiden nicht verbunden, sagt jeder etwas anderes, wie im Beispiel.

Die Lösung: Das Feld hat keine eigene Wahrheit. Es zeigt den Wert aus dem Zustand und sagt dem Zustand bei jeder Taste den neuen Text. Ein solches Feld heißt kontrolliert (englisch: controlled input).

const [name, setName] = useState('');
<input id="name" value={name} onChange={(e) => setName(e.target.value)} />

value={name} ist die Richtung vom Zustand zum Feld. onChange ist der Weg zurück: e.target.value ist der Text des Feldes im Moment des Ereignisses. In React kommt onChange bei jeder Taste, nicht erst, wenn das Feld den Fokus verliert.

Die anderen Arten von Feldern sind gleich, nur mit einer anderen Eigenschaft:

<select value={payment} onChange={(e) => setPayment(e.target.value)}>…</select>
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} />

Jetzt ist der Zustand die einzige Quelle: Was in ihm steht, steht im Feld, und umgekehrt. Der Button lässt sich über den Zustand deaktivieren (!agree), und die Anfrage schickt genau das, was der Kunde sieht.

Sieh es dir an

Sieh dir den Kreis an

Ein Buchstabe geht den ganzen Weg: Feld, Ereignis, Zustand, neues Zeichnen und wieder das Feld. Danach dasselbe Feld ohne Bindung.

function CheckoutForm() {
const [name, setName] = useState('');
return (
<form onSubmit={submit}>
<label htmlFor="name">Name</label>
<input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
);
}
Schritt für Schritt, in Worten
  1. Das Formular hält den Namen im Zustand: name = ''. Das Feld auf der Seite zeigt, was der Zustand sagt, und der sagt: leer.
  2. Der Kunde drückt M. Der Browser setzt den Buchstaben ins Feld und löst das Ereignis input aus.
  3. onChange fängt das Ereignis ab. e.target.value ist jetzt der Text des Feldes: „M“.
  4. setName("M") ändert den Zustand. Das ist der einzige Ort, an dem der Name lebt.
  5. Der Zustand hat sich geändert, also ruft React CheckoutForm erneut auf. Die Funktion gibt das Feld mit value = name zurück: „M“.
  6. Das Feld bekommt seinen Wert vom Zustand. Es ist derselbe Buchstabe, aber jetzt entscheidet der Zustand, was das Feld zeigt.
  7. Jede Taste läuft den ganzen Kreis: Feld, Ereignis, Zustand, neues Zeichnen, Feld. Eine einzige Quelle der Wahrheit: der Zustand.
  8. Jetzt dasselbe Feld ohne value und onChange. Das Feld funktioniert, aber niemand hört es ab und niemand gibt ihm einen Wert.
  9. Der Kunde tippt „Maria“. Die Buchstaben bleiben im DOM. Der Zustand sagt immer noch: name = ''.
  10. Beim Absenden schickt der Code den Zustand: { name: '' }. Der Server antwortet „Der Name ist erforderlich“, während das Feld „Maria“ zeigt.
  11. Die Regel: Jedes Feld hat value aus dem Zustand und onChange zurück dorthin. Dann können Feld und Zustand nicht auseinanderlaufen.

Der Kreis sieht wie ein unnötiger Umweg aus: Der Buchstabe steht auch ohne ihn im Feld. Aber seinetwegen können Zustand und Feld nicht auseinanderlaufen.

Probier es aus

Binde die Felder

Das Feld Name, das Select Zahlung und die Checkbox Ich akzeptiere die Bedingungen sind nicht an den Zustand gebunden. Binde alle drei. Der Button Bestellen ist deaktiviert, bis die Bedingungen akzeptiert sind. Die Prüfung tippt einen Namen, eine E-Mail-Adresse und eine Adresse ein, wählt Karte bei Lieferung, setzt und entfernt das Häkchen und bestellt dann. Sie erwartet Bestellung Nr. 61 ist angenommen. und eine Anfrage mit dem eingetippten Namen und payment: 'card'.

React

Probier es aus

Ergänze die Lücke

Was gehört in die Lücke, damit die Checkbox gebunden ist?

const [agree, setAgree] = useState(false);

<input
  type="checkbox"
  checked={agree}
  onChange={(e) => setAgree(e.target.)}
/>

Probier es aus

Finde den Fehler

Der Kunde drückt Tasten im Feld „Name“, und nichts erscheint darin. Die E-Mail funktioniert. Welche Zeile ist schuld?

Wähle die Zeile mit dem Fehler.

Die Idee

Eine Prüfung neben dem Feld

Der Server prüft jede Bestellung. Aber der Kunde soll nicht auf eine Antwort warten müssen, um zu erfahren, dass er die Adresse vergessen hat. Also prüft auch das Formular selbst, bevor es sendet.

  1. Wann. Beim Drücken auf „Bestellen“. Gibt es Fehler, geht keine Anfrage raus.
  2. Was. Eine Funktion check() gibt ein Objekt mit den Fehlern je Feld zurück: { name: 'Gib deinen Namen ein', email: … }. Ein leeres Objekt heißt „alles in Ordnung“. Es liegt im Zustand errors.
  3. Wo. Neben dem Feld, nicht in einer gemeinsamen Liste oben. Der Kunde sieht, welches Feld er korrigieren muss.
  4. Für alle. Ein Screenreader sieht nicht, dass der Text rot ist. Deshalb bekommt das Feld aria-invalid="true" (der Reader sagt „ungültig“), und die Meldung bekommt eine id, auf die das Feld mit aria-describedby zeigt. Kommt der Fokus ins Feld, liest der Reader auch die Meldung vor.
<input
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <p id="email-error" className="error">{errors.email}</p>}

Das Formular hat novalidate (in JSX noValidate). Ohne das stoppt der Browser das Absenden mit seinen eigenen Sprechblasen, bevor dein Code drankommt, und die beiden Prüfungen kommen sich in die Quere.

Der Server prüft noch einmal. Die Prüfung im Browser ist eine Bequemlichkeit für den Kunden. Sie schützt den Shop nicht: Der Code der Seite liegt beim Kunden, und eine Anfrage kann auch ohne ihn kommen. Warum das so ist, erklärt die Lektion „Nicht vertrauenswürdige Eingaben“.

Probier es aus

Prüfen vor dem Senden

Füge eine Prüfung vor dem Senden hinzu. Ein leerer Name ergibt Gib deinen Namen ein, eine E-Mail ohne @ ergibt Die E-Mail muss ein @ enthalten, eine leere Adresse ergibt Gib eine Lieferadresse ein. Jede Meldung steht neben ihrem Feld, in einem Element mit der id name-error, email-error und address-error, und das Feld hat aria-invalid=true und aria-describedby, das darauf zeigt. Bei Fehlern geht keine Anfrage raus. Die Prüfung akzeptiert die Bedingungen, bestellt mit einem leeren Formular, dann mit einer E-Mail ohne @ und zuletzt mit allem richtig.

React

Die Idee

Senden und die Antwort des Servers

Die Anfrage braucht Zeit, und der Kunde weiß nicht, ob sein Klick angekommen ist. Klickt er noch einmal, geht eine zweite Bestellung raus. Deshalb hat der Zustand auch sending:

  1. Vor fetch wird sending zu true. Der Button ist disabled, und daneben steht „Wird gesendet…“.
  2. Nach der Antwort wird sending zu false, egal was zurückkam.
  3. 201: Der Body ist { id }. Das Formular wird durch „Bestellung Nr. 61 ist angenommen.“ ersetzt.
  4. 400: Der Body ist { errors: { name, email, address, items } }, eine Meldung je Feld. Sie kommen in dasselbe errors und erscheinen so an denselben Stellen neben den Feldern. items hat kein Feld: Es erscheint neben der Zeile mit dem Warenkorb.
setSending(true);
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, address, payment, items: ITEMS }),
});
const data = await res.json();
setSending(false);
if (res.ok) setOrderId(data.id);
else setErrors(data.errors);

Die Prüfung im Browser ist einfacher als die des Servers: Sie sucht nur nach „@“. Die E-Mail maria@shop besteht sie, und der Server schickt sie mit „Die E-Mail-Adresse ist ungültig“ zurück. Das Formular muss also auch einen Fehler zeigen können, den es nicht kannte.

Genauer, für Neugierige

Unkontrollierte Felder. Ein Feld ohne value hält den Text selbst. Der Wert wird beim Absenden geholt: aus FormData oder mit einem ref. defaultValue gibt nur den Starttext vor. Das ist weniger Code, aber der Zustand weiß nicht, was der Kunde tippt, bevor er absendet. Man kann also den Button nicht über das Feld deaktivieren oder schon beim Tippen prüfen.

Von unkontrolliert zu kontrolliert. Ist value am Anfang undefined (useState() ohne Startwert) und wird später zu Text, sagt React in der Konsole A component is changing an uncontrolled input to be controlled. Der Startwert eines Textfeldes ist '', nicht undefined.

Formulare mit action in React 19. <form action={fn}> ruft fn mit den FormData des Formulars auf und leert danach die unkontrollierten Felder. Dazu gibt es auch Hooks (useActionState, useFormStatus). Das ist ein anderer Stil, näher am Formular aus der HTML-Lektion. Kontrollierte Felder bleiben der direkteste Weg, wenn die Oberfläche reagieren soll, während der Kunde tippt.

Ein Objekt statt fünf Zustände. Du kannst auch const [form, setForm] = useState({ name: '', email: '', … }) mit einer einzigen onChange-Funktion haben, die e.target.name liest. Das neue Objekt ist dann eine Kopie: setForm({ ...form, [e.target.name]: e.target.value }).

Wann prüfen. Beim Absenden ist am einfachsten. Eine Prüfung bei jeder Taste zeigt Fehler, bevor der Kunde fertig ist („die E-Mail ist ungültig“ nach dem ersten Buchstaben). Die übliche Antwort: die erste Prüfung beim Absenden oder beim Verlassen des Feldes (blur), danach bei jeder Taste, damit der Fehler verschwindet, sobald er behoben ist.

Fokus auf einen Fehler. Nach einem fehlgeschlagenen Absenden hilft es, den Fokus auf das erste Feld mit einem Fehler zu setzen. Dann kommen Tastatur und Screenreader direkt zum Problem.

Die Prüfung sieht den Code nicht. Die Aufgaben hier prüfen das Verhalten: was angezeigt wird, was deaktiviert ist, was gesendet wird. Sie sehen nicht, ob die Prüfung in einer eigenen Funktion steht oder wie die Variablen heißen.

Übung

Die Bestellung, bis zum Ende

Jetzt prüft das Formular im Browser, aber es zeigt nicht, dass es sendet, und es legt die Fehler des Servers in einen einzigen Absatz. Mach daraus eine echte Bestellung: Während die Anfrage läuft, ist der Button Bestellen deaktiviert, und es gibt einen Text Wird gesendet… (mit role=status). Bei einer Antwort 400 kommen die Fehler aus data.errors neben die Felder, an dieselben Stellen wie deine (email-error und aria-invalid an der E-Mail), und der Fehler für items neben die Zeile mit dem Warenkorb. Die Prüfung tippt die E-Mail maria@shop ein: Sie besteht deine Prüfung, aber der Server schickt sie mit Die E-Mail-Adresse ist ungültig zurück. Dann hängt sie .bg an, und die Bestellung geht durch: Bestellung Nr. 61 ist angenommen.

React

Test

Teste dich

1. Das Feld „Name“ zeigt „Maria“, und der Zustand name ist leer. Was geht an den Server?
2. Das Feld ist <input value={name} />, ohne onChange. Was passiert, wenn der Kunde tippt?
3. Wie liest du eine Checkbox in onChange?
4. Die Prüfung im Browser sagt, die E-Mail sei in Ordnung. Soll der Server sie noch einmal prüfen?
5. Warum ist der Button „Bestellen“ deaktiviert, während die Anfrage läuft?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·