Zum Inhalt springen
aviral gupta

// B5.2 · ca. 30 Min. · Einstieg

Die Struktur des State wählen

Nach dieser Lektion wählen Sie State, der sich leicht aktualisieren lässt: gruppiert, wo er sich gemeinsam ändert, ohne Widersprüche, redundante Kopien oder tiefe Verschachtelung.

Lektion 2 von 6 in B5 State verwalten

Danach können Sie

  • State gruppieren, der sich gemeinsam ändert, und Booleans, die sich widersprechen können, durch einen Status ersetzen
  • Redundanten und doppelten State entfernen: Werte beim Rendern berechnen und eine ausgewählte id halten, nicht das Objekt
  • Props nicht im State spiegeln und tief verschachtelten State zu einer Tabelle mit ids abflachen
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B5.1: ein Status-String. isEditing wird aus status berechnet. Sie klicken dreimal auf den Button. Was zeigt er?

    function Profile() {
      const [status, setStatus] = useState('viewing');
      const isEditing = status === 'editing';
    
      return (
        <button onClick={() => setStatus(isEditing ? 'viewing' : 'editing')}>
          {isEditing ? 'Save' : 'Edit'} Profile
        </button>
      );
    }
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen. Menu hält den gewählten Eintrag als Objekt. Rename ändert den ersten Eintrag der Liste. Sie klicken einmal. Was zeigen die Liste und das <p>? (Liste / <p>)

    function Menu() {
      const [items, setItems] = useState([
        { id: 0, title: 'pretzels' },
        { id: 1, title: 'granola bar' }
      ]);
      const [selectedItem, setSelectedItem] = useState(items[0]);
    
      function handleRename() {
        setItems(items.map(item =>
          item.id === 0 ? { ...item, title: 'salted pretzels' } : item
        ));
      }
    
      return (
        <>
          <ul>
            {items.map(item => <li key={item.id}>{item.title}</li>)}
          </ul>
          <p>You picked {selectedItem.title}.</p>
          <button onClick={handleRename}>Rename</button>
        </>
      );
    }
  3. Üben · Aufgabe 3 von 7

    Nur selectedId liegt im State. Ergänzen Sie die Array-Methode, die beim Rendern den ausgewählten Eintrag aus items holt.

    const selectedItem = items.____(item => item.id === selectedId);
    const selectedItem = items.(item => item.id === selectedId);
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Prinzip von react.dev die Änderung zu, die es nahelegt.

  5. Üben · Aufgabe 5 von 7

    fullName liegt im State, und nur der erste Handler aktualisiert es. Sie tippen Ada in das erste Feld, dann Lovelace in das zweite. Was zeigt das <p> (ohne Leerzeichen an den Enden)?

    function Ticket() {
      const [firstName, setFirstName] = useState('');
      const [lastName, setLastName] = useState('');
      const [fullName, setFullName] = useState('');
    
      function handleFirstNameChange(e) {
        setFirstName(e.target.value);
        setFullName(e.target.value + ' ' + lastName);
      }
    
      function handleLastNameChange(e) {
        setLastName(e.target.value);
      }
    
      return (
        <>
          <input value={firstName} onChange={handleFirstNameChange} />
          <input value={lastName} onChange={handleLastNameChange} />
          <p>Ticket for: {fullName}</p>
        </>
      );
    }
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Message kopiert seine Prop messageColor in den State. App übergibt zuerst blue. Sie klicken einmal auf Red. Was zeigen die beiden <p>?

    function Message({ messageColor }) {
      const [color] = useState(messageColor);
      return <p>Message: {color}</p>;
    }
    
    function App() {
      const [color, setColor] = useState('blue');
      return (
        <>
          <button onClick={() => setColor('red')}>Red</button>
          <p>Picked: {color}</p>
          <Message messageColor={color} />
        </>
      );
    }
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe: ein Feedback-Formular ohne Widersprüche. Schreiben Sie in main.jsx eine Komponente FeedbackForm mit einer Textarea und einem Button Send. Simulieren Sie das Senden mit einer Funktion sendMessage(text), die nach 50 ms erfüllt wird. Halten Sie statt zweier Booleans, isSending und isSent, einen status im State: 'typing', 'sending' oder 'sent'. Benennen Sie seine Teile mit Konstanten, die aus status berechnet werden. Während des Sendens deaktivieren Sie beide Bedienelemente und zeigen Sending...; nach dem Senden zeigen Sie statt des Formulars Thanks for feedback!.

    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

Ein flacher Reiseplan

Der Plan ist kein Baum verschachtelter Objekte, sondern eine Tabelle: Jeder Ort hat eine id, einen title und die ids seiner Unterorte, und plan ordnet jeder id ihren Ort zu. PlaceTree sucht einen Ort über seine id und rendert seine Unterorte auf dieselbe Weise. Das Abschließen eines Ortes ändert nur zwei Ebenen: einen neuen Elternort ohne diese id und eine neue Tabelle mit dem neuen Elternort. Ein Lauf hängt die Komponente nur ein, also schließen Timer unten in main.jsx Kenya ab, dann Europe. Im Browser bleibt die Seite aktiv: Klicken Sie selbst auf Complete.

main.jsx

import { useState } from 'react';

const initialPlan = {
  0: { id: 0, title: '(Root)', childIds: [1, 4] },
  1: { id: 1, title: 'Africa', childIds: [2, 3] },
  2: { id: 2, title: 'Egypt', childIds: [] },
  3: { id: 3, title: 'Kenya', childIds: [] },
  4: { id: 4, title: 'Europe', childIds: [5] },
  5: { id: 5, title: 'France', childIds: [] },
};

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialPlan);

  function handleComplete(parentId, childId) {
    const parent = plan[parentId];
    // A new version of the parent place
    // that doesn't include this child ID.
    const nextParent = {
      ...parent,
      childIds: parent.childIds.filter(id => id !== childId)
    };
    // A new table with the updated parent.
    setPlan({
      ...plan,
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {root.childIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => onComplete(parentId, id)}>Complete</button>
      {childIds.length > 0 && (
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      )}
    </li>
  );
}

// A run only mounts. Act like a user: complete Kenya, then Europe.
function completeButton(title) {
  const li = [...document.querySelectorAll('li')].find(li => li.firstChild.textContent === title);
  return li.querySelector('button');
}

setTimeout(() => {
  completeButton('Kenya').click();
  setTimeout(() => {
    completeButton('Europe').click();
    setTimeout(() => {
      console.log('initialPlan[1].childIds is still', initialPlan[1].childIds.join(', '));
    }, 50);
  }, 50);
}, 50);

Ausführen mit

npm run dev

Ausgabe

initialPlan[1].childIds is still 2, 3
<!-- #root -->
<div id="root"><h2>Places to visit</h2><ol><li>Africa<button>Complete</button><ol><li>Egypt<button>Complete</button></li></ol></li></ol></div>
  • Das Abschließen von Kenya erzeugte ein neues Africa mit childIds [2]; Tabelle und übrige Orte wurden unverändert übernommen.
  • Das Abschließen von Europe entfernte es aus den childIds der Wurzel, also verschwand France mit: Einen Ort, den kein Elternort listet, rendert nichts.
  • initialPlan wurde nie geändert: Jede Aktualisierung erzeugte neue Objekte.
  • Kenya, Europe und France stehen noch in der Tabelle. react.dev zeigt, wie man sie auch löscht, um Speicher zu sparen.
Ä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

Die veraltete Auswahl reparieren

Mit Menu wählen Sie einen Reisesnack und bearbeiten die Namen der Snacks. Wählen Sie einen Snack und bearbeiten Sie dann seinen Namen: Das Eingabefeld ändert sich, aber You picked … behält den alten Namen, weil der gewählte Snack als Kopie des Objekts gehalten wird. Entfernen Sie die Duplizierung. Halten Sie nur selectedId im State, mit 0 als Anfang, und suchen Sie den ausgewählten Eintrag beim Rendern in items. Choose und das Bearbeiten müssen weiter funktionieren.

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
  1. Hinweis 1

    Ersetzen Sie den State selectedItem durch const [selectedId, setSelectedId] = useState(0);.

  2. Hinweis 2

    Choose ruft setSelectedId(item.id) auf.

  3. Hinweis 3

    Im Rumpf der Komponente: const selectedItem = items.find(item => item.id === selectedId);.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item => item.id === selectedId);

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return { ...item, title: e.target.value };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              aria-label={'Snack ' + (index + 1)}
              value={item.title}
              onChange={e => handleItemChange(item.id, e)}
            />{' '}
            <button onClick={() => setSelectedId(item.id)}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}
Auf dem eigenen Computer ausführen

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.

main.jsx

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(items[0]);

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return { ...item, title: e.target.value };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              aria-label={'Snack ' + (index + 1)}
              value={item.title}
              onChange={e => handleItemChange(item.id, e)}
            />{' '}
            <button onClick={() => setSelectedItem(item)}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

main.test.jsx

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 Menu from './main.jsx';

const picked = () => screen.getByText(/^You picked/).textContent;
const choose = (n) => screen.getAllByRole('button', {name: 'Choose'})[n];
const snack = (n) => screen.getByRole('textbox', {name: 'Snack ' + n});

test('Choose wählt einen Eintrag', async () => {
  const user = userEvent.setup();
  render(<Menu />);
  assert.equal(picked(), 'You picked pretzels.', 'zu Beginn lautet die Meldung ' + JSON.stringify(picked()));
  await user.click(choose(1));
  assert.equal(picked(), 'You picked crispy seaweed.', 'nach der Wahl des zweiten Snacks lautet die Meldung ' + JSON.stringify(picked()));
});

test('Das Bearbeiten des gewählten Eintrags aktualisiert die Meldung', async () => {
  const user = userEvent.setup();
  render(<Menu />);
  await user.click(choose(1));
  await user.clear(snack(2));
  await user.type(snack(2), 'nori');
  assert.equal(picked(), 'You picked nori.', 'nach dem Bearbeiten des gewählten Snacks lautet die Meldung ' + JSON.stringify(picked()));
});

test('Das Bearbeiten des ersten Eintrags vor einer Wahl aktualisiert die Meldung', async () => {
  const user = userEvent.setup();
  render(<Menu />);
  await user.type(snack(1), ' sticks');
  assert.equal(picked(), 'You picked pretzels sticks.', 'nach dem Bearbeiten des ersten Snacks lautet die Meldung ' + JSON.stringify(picked()));
});

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

Eine Summe, die nicht auseinanderlaufen kann

ShoppingList hält die Einträge und ihre Summe in zwei State-Variablen. Add milk aktualisiert beide, aber Remove vergisst die Summe, also läuft sie auseinander. Flicken Sie nicht handleRemove: Die Summe ist redundant. Entfernen Sie den State total und berechnen Sie die Summe beim Rendern aus items, sodass jede Änderung an der Liste ohne zusätzlichen Code in Total: … € erscheint.

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
  1. Hinweis 1

    Löschen Sie const [total, setTotal] = useState(12); und den Aufruf von setTotal in handleAdd.

  2. Hinweis 2

    Fügen Sie die Summe als einfache Konstante im Rumpf der Komponente wieder ein.

  3. Hinweis 3

    const total = items.reduce((sum, item) => sum + item.price, 0);

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import { useState } from 'react';

let nextId = 3;
const initialItems = [
  { id: 0, name: 'Bread', price: 3 },
  { id: 1, name: 'Cheese', price: 5 },
  { id: 2, name: 'Apples', price: 4 },
];

export default function ShoppingList() {
  const [items, setItems] = useState(initialItems);
  const total = items.reduce((sum, item) => sum + item.price, 0);

  function handleAdd() {
    setItems([...items, { id: nextId++, name: 'Milk', price: 2 }]);
  }

  function handleRemove(id) {
    setItems(items.filter(item => item.id !== id));
  }

  return (
    <>
      <button onClick={handleAdd}>Add milk</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.name}: {item.price} €{' '}
            <button aria-label={'Remove ' + item.name} onClick={() => handleRemove(item.id)}>
              Remove
            </button>
          </li>
        ))}
      </ul>
      <p>Total: {total} €</p>
    </>
  );
}
Auf dem eigenen Computer ausführen

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.

main.jsx

import { useState } from 'react';

let nextId = 3;
const initialItems = [
  { id: 0, name: 'Bread', price: 3 },
  { id: 1, name: 'Cheese', price: 5 },
  { id: 2, name: 'Apples', price: 4 },
];

export default function ShoppingList() {
  const [items, setItems] = useState(initialItems);
  // TODO: total goes out of sync when an item is removed.
  const [total, setTotal] = useState(12);

  function handleAdd() {
    setItems([...items, { id: nextId++, name: 'Milk', price: 2 }]);
    setTotal(total + 2);
  }

  function handleRemove(id) {
    setItems(items.filter(item => item.id !== id));
  }

  return (
    <>
      <button onClick={handleAdd}>Add milk</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.name}: {item.price} €{' '}
            <button aria-label={'Remove ' + item.name} onClick={() => handleRemove(item.id)}>
              Remove
            </button>
          </li>
        ))}
      </ul>
      <p>Total: {total} €</p>
    </>
  );
}

main.test.jsx

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 ShoppingList from './main.jsx';

const total = () => screen.getByText(/^Total:/).textContent;

test('Die Summe beginnt bei der Summe der Preise', () => {
  render(<ShoppingList />);
  assert.equal(total(), 'Total: 12 €', 'zu Beginn lautet die Summe ' + JSON.stringify(total()));
});

test('Das Entfernen eines Eintrags senkt die Summe', async () => {
  const user = userEvent.setup();
  render(<ShoppingList />);
  await user.click(screen.getByRole('button', {name: 'Remove Cheese'}));
  assert.equal(total(), 'Total: 7 €', 'nach dem Entfernen von Cheese lautet die Summe ' + JSON.stringify(total()));
});

test('Milch hinzufügen und Brot entfernen lassen die Summe stimmen', async () => {
  const user = userEvent.setup();
  render(<ShoppingList />);
  await user.click(screen.getByRole('button', {name: 'Add milk'}));
  assert.equal(total(), 'Total: 14 €', 'nach dem Hinzufügen von Milch lautet die Summe ' + JSON.stringify(total()));
  await user.click(screen.getByRole('button', {name: 'Remove Bread'}));
  assert.equal(total(), 'Total: 11 €', 'nach dem Entfernen von Bread lautet die Summe ' + JSON.stringify(total()));
});

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 Feld eines gruppierten Objekts aktualisieren

import { useState } from 'react';

export default function Dot() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    <>
      <p>x: {position.x.toFixed(1)}, y: {position.y.toFixed(1)}</p>
      <button onClick={() => setPosition({ x: 100 })}>Move right</button>
    </>
  );
}

// A run only mounts. Act like a user: click once.
setTimeout(() => document.querySelector('button').click(), 50);

Was React ausgibt

TypeError: Cannot read properties of undefined (reading 'toFixed')

Warum, und die Lösung

setPosition({ x: 100 }) ersetzt das ganze Objekt, also hat die neue position kein y, und das Rendern wirft bei position.y.toFixed (Wortlaut von Chrome und Node; Firefox und Safari formulieren ihn anders). Kopieren Sie das andere Feld: setPosition({ ...position, x: 100 }), oder halten Sie x und y in zwei State-Variablen, wenn sie sich getrennt ändern.

Den Eintrag löschen, dessen id ausgewählt ist

import { useState } from 'react';

export default function Menu() {
  const [items, setItems] = useState([
    { id: 0, title: 'pretzels' },
    { id: 1, title: 'granola bar' }
  ]);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item => item.id === selectedId);

  return (
    <>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            <button onClick={() => setItems(items.filter(i => i.id !== item.id))}>
              Delete
            </button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

// A run only mounts. Act like a user: delete the picked item.
setTimeout(() => document.querySelector('li button').click(), 50);

Was React ausgibt

TypeError: Cannot read properties of undefined (reading 'title')

Warum, und die Lösung

find gibt undefined zurück, wenn kein Eintrag selectedId hat, und das Rendern wirft bei selectedItem.title (Wortlaut von Chrome und Node; Firefox und Safari formulieren ihn anders). Behandeln Sie den fehlenden Eintrag: {selectedItem ? <p>You picked {selectedItem.title}.</p> : <p>Pick a snack.</p>}, oder leeren Sie die Auswahl, wenn Sie ihn löschen.

Einen Ort aus der Tabelle entfernen, aber nicht aus seinem Elternort

import { useState } from 'react';

const initialPlan = {
  0: { id: 0, title: '(Root)', childIds: [1, 2] },
  1: { id: 1, title: 'Kenya', childIds: [] },
  2: { id: 2, title: 'Peru', childIds: [] },
};

export default function Plan() {
  const [plan, setPlan] = useState(initialPlan);

  function handleComplete(id) {
    // Removes the place from the table only.
    const nextPlan = { ...plan };
    delete nextPlan[id];
    setPlan(nextPlan);
  }

  return (
    <ol>
      {plan[0].childIds.map(id => (
        <Place key={id} id={id} plan={plan} onComplete={handleComplete} />
      ))}
    </ol>
  );
}

function Place({ id, plan, onComplete }) {
  const place = plan[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title} ({childIds.length} stops)
      <button onClick={() => onComplete(id)}>Complete</button>
    </li>
  );
}

// A run only mounts. Act like a user: complete Kenya.
setTimeout(() => document.querySelector('button').click(), 50);

Was React ausgibt

TypeError: Cannot read properties of undefined (reading 'childIds')

Warum, und die Lösung

Die Wurzel listet die id noch, also sucht Place sie in einer Tabelle, die sie nicht mehr hat, und wirft bei place.childIds (Wortlaut von Chrome und Node; Firefox und Safari formulieren ihn anders). Aktualisieren Sie zuerst den Elternort: eine neue Wurzel, deren childIds die id herausfiltern, dann eine neue Tabelle mit dieser Wurzel.

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.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Gruppieren, was sich gemeinsam ändert, und Widersprüche ausschließen

Wenn zwei State-Variablen sich immer gemeinsam ändern, etwa x und y eines Zeigers, fassen Sie sie zu einem Objekt zusammen; dann können Sie keine vergessen. Ein Objekt im State wird allerdings ganz ersetzt: setPosition({ x: 100 }) verliert y, also kopieren Sie den Rest mit { ...position, x: 100 }. Booleans, die nie gleichzeitig true sein dürfen, etwa isSending und isSent, lassen Raum für einen unmöglichen Zustand. Ersetzen Sie sie durch einen status, 'typing', 'sending' oder 'sent'. Teile davon können Sie trotzdem benennen, const isSending = status === 'sending': Diese Konstanten sind kein State und können daher nicht auseinanderlaufen.

Nur behalten, was sich nicht berechnen lässt

Was Sie beim Rendern aus Props oder anderem State berechnen können, gehört nicht in den State. fullName ist firstName + ' ' + lastName, also berechnen Sie es im Rumpf der Komponente; jeder Set-Aufruf rendert neu, und der neue Wert folgt. Eine Summe ist ein reduce über die Einträge. Duplizierung ist dasselbe Problem in anderer Form: Wer den ausgewählten Eintrag als Objekt hält, kopiert einen Eintrag der Liste, und nach einer Änderung an der Liste ist die Kopie veraltet. Halten Sie selectedId im State und suchen Sie den Eintrag beim Rendern. Kopieren Sie auch keine Prop in den State: useState verwendet sie nur beim ersten Rendern. Nennen Sie so eine Prop initialColor, wenn das Ignorieren gewollt ist.

Tief verschachtelten State abflachen

Ein Baum von Orten, jeder mit einem Array von Unterorten, ist leicht hinzuschreiben und schwer zu aktualisieren: Wer einen tiefen Ort entfernt, muss jeden Elternort darüber kopieren. Machen Sie ihn flach, also normalisiert, wie eine Datenbanktabelle: ein Objekt, das jede id einem Ort zuordnet, und jeder Ort listet nur seine childIds. Das Entfernen eines Ortes ändert dann zwei Ebenen: einen neuen Elternort, dessen childIds die id nicht mehr enthalten, und eine neue Tabelle mit diesem Elternort. Flüchtiger UI-State, etwa ob ein Eintrag gerade unter dem Mauszeiger ist, kann oft stattdessen in die Kindkomponente wandern.

Quellen

Zuletzt geprüft am 5. Oktober 2026