Nach dieser Lektion können Sie voraussagen, wann React den State einer Komponente behält und wann es ihn verwirft, und ihn gezielt mit einem key zurücksetzen.
Erklären, dass React State an eine Position im Render-Baum bindet, nicht an ein JSX-Tag
Voraussagen, wann eine Änderung den State behält und wann ein anderer Typ oder Aufbau ihn zurücksetzt
State gezielt mit verschiedenen Positionen oder einem key zurücksetzen und Komponenten auf oberster Ebene definieren
Aufwärmen · Aufgabe 1 von 7
Zum Aufwärmen aus Lektion B5.3: angehobener State. Panel besitzt count und gibt es an zwei Badges weiter. Sie klicken zweimal auf Add. Was zeigen die beiden <p>?
Scoreboard berechnet const person = isPlayerA ? 'Mia' : 'Leo'; und rendert einen Counter. Ergänzen Sie das Attribut, das jedem Spieler einen frischen Punktestand gibt.
<Counter ____={person} person={person} />
<Counter ={person} person={person} />
Üben · Aufgabe 4 von 7
Ordnen Sie jeder Änderung an einer Position zu, was mit dem State dort geschieht.
Üben · Aufgabe 5 von 7
App hat zwei return-Anweisungen. Sie klicken zweimal auf den Zähler, dann auf Fancy. Was zeigen die Buttons?
Kleine Aufgabe: ein Quiz, das jede Frage mit einer leeren Antwort beginnt. Schreiben Sie in main.jsx eine Komponente Answer, die den Text einer Frage zeigt und ein Eingabefeld, dessen Wert ihr eigener State answer ist. Schreiben Sie eine Komponente Quiz, die den Index der aktuellen Frage im State hält, ein Answer dafür rendert und einen Button Next question. Ohne key bliebe die getippte Antwort für die nächste Frage stehen; geben Sie Answer einen key, damit jede Frage leer beginnt.
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Eine Anzeigetafel, auf der jeder Spieler bei null beginnt
Die beiden Counter stehen an derselben Stelle im JSX, aber jeder hat seinen eigenen key, Mia oder Leo. React nutzt den key als Teil der Position, also entfernt ein Spielerwechsel einen Counter samt State und erzeugt den anderen von vorn. Ein Lauf hängt die Komponente nur ein, also klicken Timer unten in main.jsx die Buttons, alle 50 ms ein Schritt, und geben nach jeder Änderung die Überschrift aus. Im Browser bleibt die Seite aktiv: Klicken Sie selbst.
main.jsx
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter key="Mia" person="Mia" />
) : (
<Counter key="Leo" person="Leo" />
)}
<button onClick={() => setIsPlayerA(!isPlayerA)}>Next player</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
return (
<section>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>Add one</button>
</section>
);
}
// A run only mounts. Act like the players, one step every 50 ms.
const click = name => [...document.querySelectorAll('button')].find(b => b.textContent === name).click();
const print = () => console.log(document.querySelector('h1').textContent);
const plan = [
() => click('Add one'),
() => click('Add one'),
() => click('Add one'),
print,
() => click('Next player'),
print,
() => click('Add one'),
print,
() => click('Next player'),
print,
];
plan.forEach((step, i) => setTimeout(step, 50 * (i + 1)));
Mia erzielte 3 Punkte. Next player entfernte ihren Counter und damit seinen State.
Leos Counter begann bei 0, obwohl er an derselben Stelle im JSX rendert.
Der Wechsel zurück erzeugte Mias Counter neu, von vorn: 0, nicht 3. Ihren Punktestand zu behalten hieße, ihn in Scoreboard anzuheben.
Ohne die keys behielte der ternäre Operator einen Punktestand für beide Spieler, wie in der Aktivität davor.
Ändern und ausführen
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser React und den Compiler herunter, der JSX in JavaScript übersetzt (bis zu 2 MB), und speichert beides im Cache. Ihre Komponenten laufen in Ihrem Browser und bleiben auf Ihrem Gerät.
Ihre Seite
Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.
Führen Sie das Programm aus, um seine Seite hier zu sehen.
Übungen
Übung 1 von 2
Den Entwurf leeren, wenn der Empfänger wechselt
Messenger zeigt eine ContactList und einen Chat für den gewählten Kontakt. Tippen Sie eine Nachricht an Mia und wählen Sie dann Leo: Der Entwurf bleibt, also könnten Sie Mias Nachricht versehentlich an Leo schicken. Chat ist dieselbe Komponente an derselben Position, also behält React ihren State. Sorgen Sie dafür, dass React für jeden Kontakt einen neuen Chat erzeugt, sodass ein Wechsel immer mit einer leeren Nachricht beginnt. Ändern Sie nur, wie Messenger Chat rendert.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser React und den Compiler herunter, der JSX in JavaScript übersetzt (bis zu 2 MB), und speichert beides im Cache. Ihre Komponenten laufen in Ihrem Browser und bleiben auf Ihrem Gerät.
Ihre Seite
Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.
Führen Sie das Programm aus, um seine Seite hier zu sehen.
Hinweise
Hinweis 1
Ein key sagt React, dass zwei Komponenten an derselben Position verschieden sind.
Hinweis 2
Nehmen Sie etwas, das den Kontakt eindeutig bezeichnet, etwa seine id.
Hinweis 3
<Chat key={to.id} contact={to} />
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
Installieren Sie React 19.3 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen, fireEvent} from '@testing-library/react';
import Messenger from './main.jsx';
const message = (name) => screen.getByRole('textbox', {name: 'Message to ' + name});
const contact = (name) => screen.getByRole('button', {name});
test('Eine getippte Nachricht bleibt, solange der Kontakt bleibt', () => {
render(<Messenger />);
fireEvent.change(message('Mia'), {target: {value: 'Hi Mia'}});
assert.equal(message('Mia').value, 'Hi Mia', 'die Nachricht an Mia enthält ' + JSON.stringify(message('Mia').value));
});
test('Die Wahl eines anderen Kontakts leert den Entwurf', () => {
render(<Messenger />);
fireEvent.change(message('Mia'), {target: {value: 'Hi Mia'}});
fireEvent.click(contact('Leo'));
assert.equal(message('Leo').value, '', 'nach der Wahl von Leo enthält die Nachricht ' + JSON.stringify(message('Leo').value));
});
test('Jeder Wechsel beginnt mit einer leeren Nachricht', () => {
render(<Messenger />);
fireEvent.click(contact('Noor'));
fireEvent.change(message('Noor'), {target: {value: 'Lunch?'}});
fireEvent.click(contact('Mia'));
assert.equal(message('Mia').value, '', 'nach dem Wechsel von Noor zu Mia enthält die Nachricht ' + JSON.stringify(message('Mia').value));
assert.equal(screen.getByRole('button', {name: /^Send to/}).textContent, 'Send to mia@mail.com', 'der Senden-Button lautet ' + JSON.stringify(screen.getByRole('button', {name: /^Send to/}).textContent));
});
Legen Sie main.jsx in einem Vite-React-Projekt (npm create vite@latest, Vorlage react) als App.jsx in src/ ab und starten Sie es mit npm run dev. React rendert es dort wie hier in <StrictMode>.
Programm ausführen:
npm run dev
Für die Prüfungen gibt es noch keinen Befehl auf Ihrem Computer. Sie stehen in main.test.jsx, geschrieben mit node:test und React Testing Library.
Übung 2 von 2
Die Punkte bei ihren Spielern halten
Scoreboard zeigt Mia und Leo, und Swap ändert ihre Reihenfolge. Geben Sie Mia zwei Punkte und klicken Sie auf Swap: Jetzt hat Leo ihre 2 Punkte und Mia 0, weil der State an seiner Position bleibt, während die Personen wandern. Sorgen Sie dafür, dass jeder Punktestand bei seinem Spieler bleibt, wie oft Sie auch tauschen. Behalten Sie die beiden Zweige und den Button Swap.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser React und den Compiler herunter, der JSX in JavaScript übersetzt (bis zu 2 MB), und speichert beides im Cache. Ihre Komponenten laufen in Ihrem Browser und bleiben auf Ihrem Gerät.
Ihre Seite
Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.
Führen Sie das Programm aus, um seine Seite hier zu sehen.
Hinweise
Hinweis 1
Ohne keys ordnet React die beiden Counter nach ihrer Reihenfolge im Elternteil zu.
Hinweis 2
Keys gibt es nicht nur für Listen: Sie wirken bei allen Komponenten mit demselben Elternteil.
Hinweis 3
Geben Sie jedem Counter seinen Spieler als key, in beiden Zweigen: <Counter key="Mia" person="Mia" />.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
Installieren Sie React 19.3 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen, fireEvent} from '@testing-library/react';
import Scoreboard from './main.jsx';
const scores = () => screen.getAllByRole('heading').map((h) => h.textContent).join(', ');
const add = (person) => fireEvent.click(screen.getByRole('button', {name: 'Add one for ' + person}));
const swap = () => fireEvent.click(screen.getByRole('button', {name: 'Swap'}));
test('Jeder Zähler zählt seinen eigenen Punktestand', () => {
render(<Scoreboard />);
add('Mia');
add('Mia');
add('Leo');
assert.equal(scores(), 'Mia: 2, Leo: 1', 'nach zwei Punkten für Mia und einem für Leo lauten die Punktestände ' + JSON.stringify(scores()));
});
test('Nach einem Tausch bleiben die Punkte bei ihren Spielern', () => {
render(<Scoreboard />);
add('Mia');
add('Mia');
swap();
assert.equal(scores(), 'Leo: 0, Mia: 2', 'nach zwei Punkten für Mia und einem Tausch lauten die Punktestände ' + JSON.stringify(scores()));
});
test('Die Punkte überstehen zwei Tausche und weitere Klicks', () => {
render(<Scoreboard />);
add('Mia');
add('Mia');
swap();
add('Leo');
assert.equal(scores(), 'Leo: 1, Mia: 2', 'nach einem Tausch und einem Punkt für Leo lauten die Punktestände ' + JSON.stringify(scores()));
swap();
assert.equal(scores(), 'Mia: 2, Leo: 1', 'nach dem Zurücktauschen lauten die Punktestände ' + JSON.stringify(scores()));
});
Legen Sie main.jsx in einem Vite-React-Projekt (npm create vite@latest, Vorlage react) als App.jsx in src/ ab und starten Sie es mit npm run dev. React rendert es dort wie hier in <StrictMode>.
Programm ausführen:
npm run dev
Für die Prüfungen gibt es noch keinen Befehl auf Ihrem Computer. Sie stehen in main.test.jsx, geschrieben mit node:test und React Testing Library.
Häufige Fehler
Sich auf State des vorigen Kontakts verlassen
import { useState } from 'react';
const chats = {
Mia: [
{ id: 0, text: 'See you at six?' },
{ id: 1, text: 'Bring the map.' },
{ id: 2, text: 'Thanks!' }
],
Leo: [{ id: 0, text: 'Running late.' }]
};
export default function Messenger() {
const [name, setName] = useState('Mia');
return (
<>
<button onClick={() => setName('Mia')}>Mia</button>
<button onClick={() => setName('Leo')}>Leo</button>
<Thread messages={chats[name]} />
</>
);
}
function Thread({ messages }) {
const [index, setIndex] = useState(0);
return (
<section>
<p>{messages[index].text}</p>
<button onClick={() => setIndex(index + 1)}>Older</button>
</section>
);
}
// A run only mounts. Act like a user: read Mia's third message, then open Leo.
setTimeout(() => [...document.querySelectorAll('button')].find(b => b.textContent === 'Older').click(), 50);
setTimeout(() => [...document.querySelectorAll('button')].find(b => b.textContent === 'Older').click(), 100);
setTimeout(() => [...document.querySelectorAll('button')].find(b => b.textContent === 'Leo').click(), 150);
Was React ausgibt
TypeError: Cannot read properties of undefined (reading 'text')
Warum, und die Lösung
Thread bleibt an derselben Position, also übersteht sein index 2 den Wechsel, und Leo hat nur eine Nachricht: messages[2] ist undefined (Wortlaut von Chrome und Node; Firefox und Safari formulieren ihn anders). Geben Sie jedem Gespräch einen eigenen Thread: <Thread key={name} messages={chats[name]} />, damit index wieder bei 0 beginnt.
Eine Komponente als Funktion aufrufen
import { useState } from 'react';
export default function Scoreboard() {
const [showLeo, setShowLeo] = useState(false);
return (
<div>
{Counter({ person: 'Mia' })}
{showLeo && Counter({ person: 'Leo' })}
<button onClick={() => setShowLeo(true)}>Add Leo</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
return <h1>{person}: {score}</h1>;
}
// A run only mounts. Act like a user: add Leo.
setTimeout(() => [...document.querySelectorAll('button')].find(b => b.textContent === 'Add Leo').click(), 50);
Was React ausgibt
Rendered more hooks than during the previous render.
Warum, und die Lösung
Counter({ person: 'Mia' }) ist ein gewöhnlicher Funktionsaufruf, keine Komponente an einer Position, also gehört sein useState zu Scoreboard. Kommt Leo hinzu, bekommt Scoreboard einen Hook mehr, und React bricht ab. Rendern Sie Komponenten mit JSX, <Counter person="Mia" />, damit jede ihre eigene Position und ihren eigenen State bekommt.
Ein zurückgesetzter State, der mit einer fehlenden Prop beginnt
import { useState } from 'react';
const contacts = [
{ id: 0, name: 'Mia', draft: 'See you at six?' },
{ id: 1, name: 'Leo' }
];
export default function Messenger() {
const [to, setTo] = useState(contacts[0]);
return (
<>
{contacts.map(contact => (
<button key={contact.id} onClick={() => setTo(contact)}>
{contact.name}
</button>
))}
<Chat key={to.id} contact={to} />
</>
);
}
function Chat({ contact }) {
const [text, setText] = useState(contact.draft);
return (
<section>
<textarea value={text} onChange={e => setText(e.target.value)} />
<p>{text.length} / 280</p>
</section>
);
}
// A run only mounts. Act like a user: open Leo.
setTimeout(() => [...document.querySelectorAll('button')].find(b => b.textContent === 'Leo').click(), 50);
Was React ausgibt
TypeError: Cannot read properties of undefined (reading 'length')
Warum, und die Lösung
Der key wirkt: Die Wahl von Leo erzeugt einen neuen Chat, und sein useState läuft erneut. Aber Leo hat keinen draft, also beginnt text mit undefined, und text.length wirft (Wortlaut von Chrome und Node; Firefox und Safari formulieren ihn anders). Machen Sie den Anfangswert für jeden Kontakt gültig: useState(contact.draft ?? '').
React im Browser: React 19.3.0 (Entwicklungs-Build) und React Testing Library, MIT, von dieser Website ausgeliefert; JSX übersetzt TypeScript 6.0.3, Apache-2.0. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.
Kernideen
State gehört zu einer Position im Baum
State steckt weder im JSX-Tag noch in der Komponentenfunktion. React hält ihn und bindet ihn an die Stelle, an der die Komponente im Render-Baum sitzt. Zwei <Counter /> nebeneinander sind zwei Positionen, also bekommt jeder seinen eigenen score, selbst wenn beide aus einer JSX-Variablen stammen. Wird eine Komponente nicht mehr gerendert, zerstört React ihren State; kommt sie zurück, beginnt ihr State von vorn. Solange dieselbe Komponente an derselben Position gerendert wird, behält React den State, welche Props sie auch bekommt. Auch zwei return-Anweisungen in einem if ergeben dieselbe Position, wenn der zurückgegebene Baum dieselbe Form hat.
Ein anderer Typ an derselben Position setzt den Teilbaum zurück
Ersetzt ein <p> den <Counter> an einer Position, entfernt React den Counter und zerstört seinen State. Dasselbe gilt für alles darunter: Wird eine <section>, die einen Counter enthält, zu einem <div>, wird der ganze Teilbaum neu aufgebaut, und der Counter beginnt bei 0. Damit State zwischen Renderdurchläufen bleibt, muss der Aufbau des Baums von einem Rendern zum nächsten übereinstimmen. Deshalb dürfen Komponentendefinitionen auch nicht verschachtelt werden: Eine Komponentenfunktion, die in einer anderen deklariert ist, ist bei jedem Rendern eine neue Funktion, also sieht React an dieser Position einen anderen Typ und setzt ihren State jedes Mal zurück.
Gezielt zurücksetzen: andere Positionen oder ein key
{isPlayerA ? <Counter person="Mia" /> : <Counter person="Leo" />} behält einen Punktestand für beide Spieler, weil beide dieselbe Komponente an derselben Position sind. Um ihn zurückzusetzen, rendern Sie sie an zwei Positionen, {isPlayerA && …}{!isPlayerA && …}, oder geben Sie jedem mit einem key eine Identität: <Counter key="Mia" … />. React nutzt den key dann als Teil der Position statt der Reihenfolge im Elternteil. Keys müssen nur unter Geschwistern eindeutig sein. Ein key ist der übliche Weg, ein Formular zurückzusetzen: <Chat key={to.id} contact={to} /> leert den Entwurf, wenn der Empfänger wechselt. Den Entwurf eines entfernten Chats zu behalten hieße, ihn anzuheben; das behandelt diese Lektion nicht.