Nach dieser Lektion aktualisieren Sie Objekte und Arrays im State, indem Sie neue erzeugen: Spread für Objekte; Spread, filter, map und slice für Arrays.
Ein Objekt im State durch eine Kopie per Spread ersetzen, auch ein verschachteltes, und viele Felder mit einem Handler bedienen
Einträge eines Arrays im State hinzufügen, entfernen, ersetzen, einfügen und umordnen, ohne das Array zu mutieren
Ein Objekt in einem Array mit map aktualisieren, sodass keine andere Liste und kein anderes Rendern die Änderung sieht
Aufwärmen · Aufgabe 1 von 7
Zum Aufwärmen aus Lektion B3.4: State als Schnappschuss. Der Handler setzt tags zweimal, jedes Mal mit einem neuen Array aus tags. Sie klicken einmal. Was zeigt der Button?
Ein Handler bedient beide Eingabefelder, <input name="first"> und <input name="last">. Ergänzen Sie den Schlüssel, sodass der Handler das Feld ändert, das nach dem geänderten Eingabefeld benannt ist, und das andere behält.
function handleChange(e) {
setPerson({
...person,
____: e.target.value
});
}
: e.target.value
Üben · Aufgabe 4 von 7
Ordnen Sie jeder Änderung an einem Array im State die Schreibweise zu, die ein neues Array erzeugt.
Üben · Aufgabe 5 von 7
Der Handler kopiert person, schreibt aber ein neues artwork nur mit city. Sie klicken auf Move. Was zeigt das <p> (ohne Leerzeichen an den Enden)?
Knobelaufgabe. Zwei Listen beginnen mit demselben Array. Der Handler kopiert myList, ändert den ersten Eintrag und setzt myList. Sie klicken einmal. Was zeigen die beiden <p>?
Mini-Aufgabe: eine Gästeliste. Schreiben Sie in main.jsx eine Komponente GuestList mit einem Eingabefeld und einem Button Add, der den getippten Namen hinten an ein Array guests anfügt (jeder Gast ein Objekt mit id und name) und das Feld leert. Jeder Gast bekommt einen Button Remove. Ein Button Sort A–Z sortiert die Gäste nach Namen. Mutieren Sie das State-Array nie. Führen Sie es aus: Fügen Sie Zoe, Ben und Mia hinzu, sortieren Sie, und entfernen Sie dann Ben.
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 Playlist: liken, einfügen und umkehren ohne Mutation
Playlist hält ein Array von Song-Objekten. Like erzeugt mit map ein neues Array und nur für den gelikten Song ein neues Objekt. Insert setzt einen Song mit zwei slice-Aufrufen und Spread an Index 1. Reverse kopiert das Array, bevor es umgekehrt wird. Ein Lauf hängt die Komponente nur ein, also klicken Timer unten in main.jsx auf Like bei Sunrise, dann auf Insert, dann auf Reverse, und geben den ersten Eintrag des Anfangsarrays aus. Im Browser bleibt die Seite aktiv: Klicken Sie die Buttons selbst.
main.jsx
import { useState } from 'react';
let nextId = 3;
const initialSongs = [
{ id: 0, title: 'Sunrise', likes: 0 },
{ id: 1, title: 'Night drive', likes: 2 },
{ id: 2, title: 'Rainy day', likes: 5 },
];
export default function Playlist() {
const [songs, setSongs] = useState(initialSongs);
function handleLike(songId) {
setSongs(songs.map(song => {
if (song.id === songId) {
// A new object with one field changed
return { ...song, likes: song.likes + 1 };
} else {
// No change
return song;
}
}));
}
function handleInsert() {
const insertAt = 1;
setSongs([
...songs.slice(0, insertAt),
{ id: nextId++, title: 'Interlude', likes: 0 },
...songs.slice(insertAt)
]);
}
function handleReverse() {
const nextSongs = [...songs];
nextSongs.reverse();
setSongs(nextSongs);
}
return (
<>
<button onClick={handleInsert}>Insert</button>
<button onClick={handleReverse}>Reverse</button>
<ul>
{songs.map(song => (
<li key={song.id}>
{song.title} ({song.likes}){' '}
<button onClick={() => handleLike(song.id)}>Like</button>
</li>
))}
</ul>
</>
);
}
// A run only mounts. Act like a user: like Sunrise, insert, then reverse.
setTimeout(() => {
document.querySelector('li button').click();
setTimeout(() => {
document.querySelector('button').click();
setTimeout(() => {
document.querySelectorAll('button')[1].click();
setTimeout(() => {
console.log('initialSongs[0].likes is still', initialSongs[0].likes);
}, 50);
}, 50);
}, 50);
}, 50);
Ausführen mit
npm run dev
Ausgabe
initialSongs[0].likes is still 0
<!-- #root -->
<div id="root"><button>Insert</button><button>Reverse</button><ul><li>Rainy day (5) <button>Like</button></li><li>Night drive (2) <button>Like</button></li><li>Interlude (0) <button>Like</button></li><li>Sunrise (1) <button>Like</button></li></ul></div>
Sunrise zeigt 1 Like, aber initialSongs[0].likes ist noch 0: map hat dafür ein neues Objekt erzeugt, statt das alte zu ändern.
Interlude kam an Index 1, zwischen Sunrise und Night drive, bevor die Liste umgekehrt wurde.
Reverse hat nur die Kopie nextSongs geändert; songs aus dem früheren Rendern behält seine Reihenfolge.
Die Keys sind die ids der Songs, also behält React jedes <li> bei seinem Song, wenn sich die Reihenfolge ändert.
Ä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
Ein Formular mit verschachtelter Adresse reparieren
ProfileForm bearbeitet eine Person mit verschachtelter Adresse. Tippen in ein beliebiges Feld ändert nichts auf dem Bildschirm, weil beide Handler das State-Objekt mutieren. Schreiben Sie sie neu. handleChange aktualisiert name oder email mit einer Kopie und einem berechneten Schlüssel, [e.target.name]. handleAddressChange aktualisiert street oder city: Kopieren Sie die Adresse und die Person. Lassen Sie initialPerson exportiert und unverändert.
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
Jedes Eingabefeld hat einen name, der zu seinem Schlüssel passt: name, email, street, city.
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} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ProfileForm, {initialPerson} from './main.jsx';
const field = (name) => screen.getByRole('textbox', {name});
const summary = () => screen.getByText(/lives at/).textContent;
test('Ein neuer Name wird angezeigt', async () => {
const user = userEvent.setup();
render(<ProfileForm />);
await user.clear(field('Name:'));
await user.type(field('Name:'), 'Grace Hopper');
assert.equal(field('Name:').value, 'Grace Hopper', 'nach dem Tippen enthält das Feld Name ' + JSON.stringify(field('Name:').value));
assert.equal(summary(), 'Grace Hopper (ada@example.com) lives at 1 Main Street, London.', 'nach dem Tippen eines Namens lautet die Zusammenfassung ' + JSON.stringify(summary()));
});
test('Eine neue Stadt behält Namen und Straße', async () => {
const user = userEvent.setup();
render(<ProfileForm />);
await user.clear(field('City:'));
await user.type(field('City:'), 'Paris');
assert.equal(summary(), 'Ada Lovelace (ada@example.com) lives at 1 Main Street, Paris.', 'nach dem Tippen einer Stadt lautet die Zusammenfassung ' + JSON.stringify(summary()));
});
test('Das Anfangsobjekt bleibt unverändert', async () => {
const user = userEvent.setup();
render(<ProfileForm />);
await user.type(field('Email:'), '.uk');
await user.type(field('Street:'), ' West');
assert.equal(initialPerson.email, 'ada@example.com', 'nach dem Tippen ist initialPerson.email ' + JSON.stringify(initialPerson.email));
assert.equal(initialPerson.address.street, '1 Main Street', 'nach dem Tippen ist initialPerson.address.street ' + JSON.stringify(initialPerson.address.street));
});
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
Todos hinzufügen, abhaken und löschen
TodoList zeigt drei Todos. Add schiebt in das State-Array, also erscheint nichts; eine Checkbox ändert ihr Todo-Objekt, also bleibt der Haken nicht; Delete tut nichts. Reparieren Sie alle drei ohne Mutation: Add fügt ein neues Todo mit dem getippten Titel hinten an und leert das Feld, eine Checkbox schaltet done nur für ihr eigenes Todo um, und Delete entfernt sein eigenes Todo. Lassen Sie initialTodos exportiert und unverändert.
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.
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, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TodoList, {initialTodos} from './main.jsx';
const titles = () => screen.queryAllByRole('listitem').map((li) => li.textContent.replace('Delete', ''));
const box = (name) => screen.getByRole('checkbox', {name});
const deleteButton = (title) => within(screen.getByText(title).closest('li')).getByRole('button', {name: 'Delete'});
test('Add hängt das neue Todo hinten an und leert das Feld', async () => {
const user = userEvent.setup();
render(<TodoList />);
await user.type(screen.getByRole('textbox', {name: 'New todo:'}), 'Walk the dog');
await user.click(screen.getByRole('button', {name: 'Add'}));
assert.deepEqual(titles(), ['Buy milk', 'Water plants', 'Call Grandma', 'Walk the dog'], 'nach Add lautet die Liste ' + JSON.stringify(titles()));
assert.equal(screen.getByRole('textbox', {name: 'New todo:'}).value, '', 'nach Add enthält das Feld ' + JSON.stringify(screen.getByRole('textbox', {name: 'New todo:'}).value));
});
test('Eine Checkbox schaltet nur ihr eigenes Todo um', async () => {
const user = userEvent.setup();
render(<TodoList />);
await user.click(box('Water plants'));
assert.equal(box('Water plants').checked, true, 'nach einem Klick ist Water plants angehakt: ' + box('Water plants').checked);
assert.equal(box('Call Grandma').checked, false, 'Call Grandma ist angehakt: ' + box('Call Grandma').checked);
await user.click(box('Buy milk'));
assert.equal(box('Buy milk').checked, false, 'nach einem Klick ist Buy milk angehakt: ' + box('Buy milk').checked);
});
test('Delete entfernt nur sein eigenes Todo', async () => {
const user = userEvent.setup();
render(<TodoList />);
await user.click(deleteButton('Water plants'));
assert.deepEqual(titles(), ['Buy milk', 'Call Grandma'], 'nach dem Löschen von Water plants lautet die Liste ' + JSON.stringify(titles()));
});
test('Das Anfangsarray und seine Objekte bleiben unverändert', async () => {
const user = userEvent.setup();
render(<TodoList />);
await user.click(box('Call Grandma'));
await user.type(screen.getByRole('textbox', {name: 'New todo:'}), 'Read');
await user.click(screen.getByRole('button', {name: 'Add'}));
assert.equal(initialTodos.length, 3, 'nach Add hat initialTodos ' + initialTodos.length + ' Einträge');
assert.equal(initialTodos[2].done, false, 'nach dem Klick ist initialTodos[2].done ' + initialTodos[2].done);
});
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
Ein Objekt setzen, ohne die übrigen Felder zu kopieren
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Ada',
lastName: 'Lovelace'
});
function handleChange(e) {
setPerson({ [e.target.name]: e.target.value });
}
return (
<>
<input name="firstName" value={person.firstName} onChange={handleChange} />
<input name="lastName" value={person.lastName} onChange={handleChange} />
</>
);
}
// A run only mounts. Act like a user: type into the first field.
setTimeout(() => {
const input = document.querySelector('input');
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
setValue.call(input, 'Grace');
input.dispatchEvent(new window.Event('input', { bubbles: true }));
}, 50);
Was React ausgibt
A component is changing a controlled input to be uncontrolled.
Warum, und die Lösung
Das neue Objekt hat nur firstName, also wird person.lastName undefined, und das zweite Eingabefeld wechselt von kontrolliert zu unkontrolliert; React warnt. Ein Set-Aufruf ersetzt das ganze Objekt; er führt nichts zusammen. Kopieren Sie zuerst die übrigen Felder: setPerson({ ...person, [e.target.name]: e.target.value }).
Ein Objekt mit eckigen Klammern kopieren
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({ name: 'Ada', city: 'London' });
function handleClick() {
setPerson([...person, { city: 'Paris' }]);
}
return <button onClick={handleClick}>{person.name} in {person.city}</button>;
}
// Act like a user: click once.
setTimeout(() => document.querySelector('button').click(), 50);
Was React ausgibt
TypeError: person is not iterable
Warum, und die Lösung
[...person] ist die Array-Spread-Syntax, und ein einfaches Objekt lässt sich nicht in ein Array ausbreiten; der Klick wirft also einen Fehler (Wortlaut von Chrome und Node; Firefox und Safari formulieren ihn anders). Kopieren Sie ein Objekt mit geschweiften Klammern und überschreiben Sie das Feld: setPerson({ ...person, city: 'Paris' }).
Ein map-Callback, der nur den geänderten Eintrag zurückgibt
TypeError: Cannot read properties of undefined (reading 'done')
Warum, und die Lösung
map legt in das neue Array, was der Callback zurückgibt. Für jedes Todo außer dem angeklickten gibt dieser Callback nichts zurück, also enthält das neue Array undefined, und das Rendern wirft bei todo.done (Wortlaut von Chrome und Node; Firefox und Safari formulieren ihn anders). Geben Sie für die anderen das alte Objekt zurück: Ergänzen Sie return todo; nach dem if.
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 als schreibgeschützt behandeln und Objekte ersetzen
Ein Objekt im State lässt sich ändern, position.x = 5, aber React erfährt nichts davon: Nichts wird neu gerendert, und der Schnappschuss des alten Renderns ändert sich unbemerkt. Behandeln Sie jedes Objekt im State als schreibgeschützt: Erzeugen Sie ein neues Objekt und übergeben Sie es der Set-Funktion. Um die übrigen Felder zu behalten, kopieren Sie sie mit der Objekt-Spread-Syntax: setPerson({ ...person, firstName: e.target.value }). Ein Handler kann viele Eingabefelder bedienen, wenn jedes einen name hat: [e.target.name]: e.target.value verwendet diesen Namen als Schlüssel. Ein Objekt, das Sie gerade erst erzeugt haben, dürfen Sie ändern, denn noch verweist nichts anderes darauf.
Verschachtelte Objekte: jede Ebene kopieren
Spread ist flach: Es kopiert nur eine Ebene tief. In { ...person } ist person.artwork noch genau dasselbe Objekt; wer artwork der Kopie ändert, ändert also auch den State. Um person.artwork.city zu ändern, erzeugen Sie ein neues artwork und eine neue person, die darauf zeigt: setPerson({ ...person, artwork: { ...person.artwork, city: 'Berlin' } }). Kopieren Sie von der geänderten Stelle bis ganz nach oben. Ein verschachteltes Objekt ist in Wahrheit ein eigenes Objekt, auf das ein anderes zeigt, und auch weitere Objekte können darauf zeigen. Für tief verschachtelten State lässt die Bibliothek Immer Sie mutierenden Code auf einem Entwurf schreiben und erzeugt die Kopien; sie läuft nur auf Ihrem eigenen Rechner, nicht im Runner dieses Kurses.
Arrays: ein neues Array erzeugen
Arrays sind auch Objekte, also behandeln Sie sie als schreibgeschützt. Vermeiden Sie push, unshift, pop, shift, splice, arr[i] = … sowie sort oder reverse auf dem State; bevorzugen Sie Methoden, die ein neues Array zurückgeben. Hinzufügen mit [...arr, item] oder [item, ...arr]. Entfernen mit filter. Einträge umwandeln oder ersetzen mit map. Einfügen mit [...arr.slice(0, i), item, ...arr.slice(i)]. Zum Sortieren oder Umkehren erst kopieren, [...arr], dann die Kopie ändern. Auch diese Kopie ist flach: Um ein Objekt im Array zu ändern, bilden Sie mit map für diesen Eintrag ein neues Objekt, { ...item, seen: true }, und geben die anderen unverändert zurück.