Zum Inhalt springen
aviral gupta

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

State: das Gedächtnis einer Komponente

Nach dieser Lektion ersetzen Sie vergessliche lokale Variablen durch State, geben einer Komponente mehrere State-Variablen und wissen, warum jede Kopie für sich allein speichert.

Lektion 2 von 6 in B3 Interaktivität hinzufügen

Danach können Sie

  • Erklären, warum eine lokale Variable keinen Wert zwischen Rendervorgängen behält, und stattdessen State verwenden
  • Eine oder mehrere State-Variablen mit useState deklarieren und dabei die Namens- und Hook-Regeln befolgen
  • Vorhersagen, dass jede gerenderte Instanz einer Komponente ihren eigenen privaten State behält
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B3.1: Propagation. Der Delete-Button sitzt in einem Listeneintrag mit eigenem onClick. Sie klicken auf Delete. Was wird ausgegeben?

    function Row() {
      return (
        <li onClick={() => console.log('row')}>
          <button
            onClick={e => {
              e.stopPropagation();
              console.log('delete');
            }}
          >
            Delete
          </button>
        </li>
      );
    }
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen. Stepper hält clicks im State und step in einer lokalen Variablen. Sie klicken dreimal. Was zeigt der Button?

    function Stepper() {
      const [clicks, setClicks] = useState(0);
      let step = 0;
    
      function handleClick() {
        step = step + 1;
        setClicks(clicks + 1);
      }
    
      return (
        <button onClick={handleClick}>
          clicks {clicks}, step {step}
        </button>
      );
    }
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den Aufruf, der showMore umschaltet, sodass der Button die Details zeigt.

    function Details() {
      const [showMore, setShowMore] = useState(false);
    
      function handleMoreClick() {
        ____(!showMore);
      }
    
      return (
        <>
          <button onClick={handleMoreClick}>{showMore ? 'Hide' : 'Show'}</button>
          {showMore && <p>A calm, deep blue-green.</p>}
        </>
      );
    }
    (!showMore);
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Stück Code zu, was es ist.

  5. Üben · Aufgabe 5 von 7

    Viewer hat zwei State-Variablen. Sie klicken auf Details, dann auf Next. Was zeigt der Absatz?

    const words = ['alpha', 'beta', 'gamma'];
    
    function Viewer() {
      const [index, setIndex] = useState(0);
      const [showMore, setShowMore] = useState(false);
    
      return (
        <div>
          <button onClick={() => setIndex(index + 1)}>Next</button>
          <button onClick={() => setShowMore(!showMore)}>Details</button>
          <p>
            {words[index]}
            {showMore && ' (details on)'}
          </p>
        </div>
      );
    }
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Jeder Likes-Button zeigt seinen eigenen State und eine gemeinsame Modulvariable. Sie klicken zweimal auf den ersten Button, dann einmal auf den zweiten. Was zeigen die beiden Buttons?

    let shared = 0;
    
    function Likes() {
      const [own, setOwn] = useState(0);
    
      function handleClick() {
        shared = shared + 1;
        setOwn(own + 1);
      }
    
      return <button onClick={handleClick}>{own}/{shared}</button>;
    }
    
    function Page() {
      return (
        <>
          <Likes />
          <Likes />
        </>
      );
    }
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe: Lernkarten. Schreiben Sie in main.jsx eine Komponente Flashcards über ein Array von Karten, jede mit einer Frage und einer Antwort. Sie hält zwei State-Variablen: index, die aktuelle Karte, und showAnswer. Sie zeigt die Frage in einem <h2>, dann entweder die Antwort oder einen Button Show answer, dann einen Button Next. Next geht zur nächsten Karte, nach der letzten zurück zur ersten, und verbirgt die Antwort wieder. Führen Sie es aus, zeigen Sie eine Antwort und klicken Sie dann auf Next.

    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 Farbgalerie mit zwei State-Variablen

Gallery zeigt jeweils eine Farbe aus data.js. index, die aktuelle Farbe, und showMore, ob die Details zu sehen sind, sind zwei State-Variablen. Next zählt index weiter, nach der letzten Farbe zurück auf 0; Show details schaltet showMore um. Ein Lauf zeigt nur das erste Rendern, die Ausgabe ist also Tomato mit verborgenen Details. Im Browser bleibt die Seite aktiv: Klicken Sie auf Next und Show details, um den State sich ändern zu sehen.

main.jsx

import { useState } from 'react';
import { colours } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    if (index < colours.length - 1) {
      setIndex(index + 1);
    } else {
      setIndex(0);
    }
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  const colour = colours[index];
  return (
    <section>
      <button onClick={handleNextClick}>Next</button>
      <h2>{colour.name}</h2>
      <p>
        ({index + 1} of {colours.length})
      </p>
      <div className="swatch" style={{ background: colour.hex }} />
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{colour.details}</p>}
    </section>
  );
}

data.js

export const colours = [
  { name: 'Tomato', hex: '#ff6347', details: 'A warm red with a touch of orange.' },
  { name: 'Teal', hex: '#008080', details: 'A calm, deep blue-green.' },
  { name: 'Gold', hex: '#ffd700', details: 'A bright yellow with a warm glow.' }
];

styles.css

.swatch {
  width: 120px;
  height: 80px;
  border-radius: 8px;
  margin: 8px 0;
}

Ausführen mit

npm run dev

Ausgabe

<!-- #root -->
<div id="root"><section><button>Next</button><h2>Tomato</h2><p>(1 of 3)</p><div class="swatch" style="background: rgb(255, 99, 71);"></div><button>Show details</button></section></div>
  • useState(0) und useState(false) liefern beim ersten Rendern die Anfangswerte: die erste Farbe, Details verborgen.
  • jsdom schreibt die Farbe #ff6347 als rgb(255, 99, 71); ein Browser tut dasselbe im HTML, das er zeigt.
  • false rendert nichts, der Absatz mit den Details fehlt also, bis showMore true ist.
  • Nach einem Klick auf Next rendert React Gallery erneut, und useState(0) gibt 1 zurück: React merkt sich den Wert, nicht die 0.
Ä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

Der Galerie ein Gedächtnis geben

Page rendert zwei Farbgalerien. Führen Sie es aus und klicken Sie auf Next: Nichts ändert sich, weil Gallery index in einer lokalen Variablen hält. Importieren Sie useState aus react, halten Sie index stattdessen im State, beginnend bei 0, und ändern Sie ihn in handleNextClick mit dem Setter. Next auf der letzten Farbe muss zur ersten zurückspringen. Die beiden Galerien müssen getrennte Indizes behalten. Behalten Sie das export vor Gallery.

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

    Fügen Sie oben in main.jsx import { useState } from 'react'; hinzu.

  2. Hinweis 2

    Ersetzen Sie let index = 0; durch const [index, setIndex] = useState(0);.

  3. Hinweis 3

    Schreiben Sie in handleNextClick setIndex(index + 1) und setIndex(0), statt index etwas zuzuweisen.

Eine Lösung zeigen

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

import { useState } from 'react';
import { colours } from './data.js';

export function Gallery() {
  const [index, setIndex] = useState(0);

  function handleNextClick() {
    if (index < colours.length - 1) {
      setIndex(index + 1);
    } else {
      setIndex(0);
    }
  }

  const colour = colours[index];
  return (
    <section>
      <h2>{colour.name}</h2>
      <p>
        ({index + 1} of {colours.length})
      </p>
      <button onClick={handleNextClick}>Next</button>
    </section>
  );
}

export default function Page() {
  return (
    <>
      <Gallery />
      <Gallery />
    </>
  );
}
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 { colours } from './data.js';

export function Gallery() {
  // TODO: a local variable is not enough. Keep index in state.
  let index = 0;

  function handleNextClick() {
    if (index < colours.length - 1) {
      index = index + 1;
    } else {
      index = 0;
    }
  }

  const colour = colours[index];
  return (
    <section>
      <h2>{colour.name}</h2>
      <p>
        ({index + 1} of {colours.length})
      </p>
      <button onClick={handleNextClick}>Next</button>
    </section>
  );
}

export default function Page() {
  return (
    <>
      <Gallery />
      <Gallery />
    </>
  );
}

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

const names = () => screen.getAllByRole('heading', {level: 2}).map((h) => h.textContent);
const nextButtons = () => screen.getAllByRole('button', {name: 'Next'});

test('Next zeigt die zweite Farbe', async () => {
  const user = userEvent.setup();
  render(<Page />);
  await user.click(nextButtons()[0]);
  assert.deepEqual(names(), ['Teal', 'Tomato'], 'nach einem Klick auf das erste Next zeigen die Galerien ' + JSON.stringify(names()));
});

test('Next auf der letzten Farbe springt zur ersten zurück', async () => {
  const user = userEvent.setup();
  render(<Page />);
  for (let i = 0; i < 3; i++) await user.click(nextButtons()[0]);
  assert.equal(names()[0], 'Tomato', 'nach drei Klicks zeigt die erste Galerie ' + JSON.stringify(names()[0]));
  const count = screen.getAllByText(/of 3/)[0].textContent;
  assert.equal(count, '(1 of 3)', 'nach drei Klicks lautet der erste Zähler ' + JSON.stringify(count));
});

test('Jede Galerie behält ihren eigenen Index', async () => {
  const user = userEvent.setup();
  render(<Page />);
  await user.click(nextButtons()[1]);
  await user.click(nextButtons()[0]);
  await user.click(nextButtons()[0]);
  assert.deepEqual(names(), ['Gold', 'Teal'], 'nach zwei Klicks auf das erste Next und einem auf das zweite zeigen die Galerien ' + JSON.stringify(names()));
});

data.js

export const colours = [
  { name: 'Tomato', hex: '#ff6347', details: 'A warm red with a touch of orange.' },
  { name: 'Teal', hex: '#008080', details: 'A calm, deep blue-green.' },
  { name: 'Gold', hex: '#ffd700', details: 'A bright yellow with a warm glow.' }
];

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

Einen Previous-Button hinzufügen

Die Galerie kann nur vorwärts. Fügen Sie vor Next einen Button Previous hinzu. Ein Klick darauf zeigt die vorige Farbe. Auf der ersten Farbe ist er deaktiviert, mit disabled={index === 0}, sodass index nie unter 0 fällt. Schreiben Sie ein handlePreviousClick, das index nur verringert, wenn er über 0 liegt. Next und Show details 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

    handlePreviousClick ähnelt handleNextClick: if (index > 0) { setIndex(index - 1); }.

  2. Hinweis 2

    Setzen Sie <button onClick={handlePreviousClick} disabled={index === 0}>Previous</button> vor den Button Next.

  3. Hinweis 3

    Ein deaktivierter Button ignoriert Klicks, und die Prüfung im Handler hält index ohnehin bei 0 oder darüber.

Eine Lösung zeigen

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

import { useState } from 'react';
import { colours } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    if (index < colours.length - 1) {
      setIndex(index + 1);
    } else {
      setIndex(0);
    }
  }

  function handlePreviousClick() {
    if (index > 0) {
      setIndex(index - 1);
    }
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  const colour = colours[index];
  return (
    <section>
      <button onClick={handlePreviousClick} disabled={index === 0}>
        Previous
      </button>
      <button onClick={handleNextClick}>Next</button>
      <h2>{colour.name}</h2>
      <p>
        ({index + 1} of {colours.length})
      </p>
      <div className="swatch" style={{ background: colour.hex }} />
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{colour.details}</p>}
    </section>
  );
}
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';
import { colours } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    if (index < colours.length - 1) {
      setIndex(index + 1);
    } else {
      setIndex(0);
    }
  }

  // TODO: add handlePreviousClick and a Previous button before Next.
  // Disable it on the first colour, so index never goes below 0.

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  const colour = colours[index];
  return (
    <section>
      <button onClick={handleNextClick}>Next</button>
      <h2>{colour.name}</h2>
      <p>
        ({index + 1} of {colours.length})
      </p>
      <div className="swatch" style={{ background: colour.hex }} />
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{colour.details}</p>}
    </section>
  );
}

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

const name = () => screen.getByRole('heading', {level: 2}).textContent;
const button = (label) => screen.getByRole('button', {name: label});

test('Previous ist auf der ersten Farbe deaktiviert', () => {
  render(<Gallery />);
  assert.equal(button('Previous').disabled, true, 'Previous lässt sich auf der ersten Farbe klicken');
});

test('Next, dann Previous, zeigt wieder die erste Farbe', async () => {
  const user = userEvent.setup();
  render(<Gallery />);
  await user.click(button('Next'));
  assert.equal(button('Previous').disabled, false, 'Previous ist auf der zweiten Farbe noch deaktiviert');
  await user.click(button('Previous'));
  assert.equal(name(), 'Tomato', 'nach Next und Previous zeigt die Galerie ' + JSON.stringify(name()));
});

test('Zweimal Next, dann Previous, zeigt Teal, 2 von 3', async () => {
  const user = userEvent.setup();
  render(<Gallery />);
  await user.click(button('Next'));
  await user.click(button('Next'));
  await user.click(button('Previous'));
  assert.equal(name(), 'Teal', 'die Galerie zeigt ' + JSON.stringify(name()));
  const count = screen.getByText(/of 3/).textContent;
  assert.equal(count, '(2 of 3)', 'der Zähler lautet ' + JSON.stringify(count));
});

test('Show details funktioniert weiterhin', async () => {
  const user = userEvent.setup();
  render(<Gallery />);
  await user.click(button('Show details'));
  assert.ok(screen.getByText('A warm red with a touch of orange.'), 'die Details von Tomato werden nicht gezeigt');
});

data.js

export const colours = [
  { name: 'Tomato', hex: '#ff6347', details: 'A warm red with a touch of orange.' },
  { name: 'Teal', hex: '#008080', details: 'A calm, deep blue-green.' },
  { name: 'Gold', hex: '#ffd700', details: 'A bright yellow with a warm glow.' }
];

styles.css

.swatch {
  width: 120px;
  height: 80px;
  border-radius: 8px;
  margin: 8px 0;
}

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

Das Paar vertauschen

import { useState } from 'react';

export default function Counter() {
  const [setCount, count] = useState(0);

  return <p>Count: {count}</p>;
}

Was React ausgibt

Functions are not valid as a React child.

Warum, und die Lösung

useState gibt immer zuerst den Wert und dann den Setter zurück; die Namen ändern daran nichts. Hier enthält count den Setter, eine Funktion, die React nicht anzeigen kann. Halten Sie die Reihenfolge ein: const [count, setCount] = useState(0);.

Vergessen, useState aufzurufen

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState;

  return <p>Count: {count}</p>;
}

Was React ausgibt

TypeError: useState is not iterable

Warum, und die Lösung

Ohne Klammern ist useState die Funktion selbst, die sich nicht in ein Paar destrukturieren lässt, und der Lauf stoppt mit Uncaught TypeError (Formulierung von Chrome und Node.js; Firefox und Safari formulieren es anders). Rufen Sie es mit dem Anfangswert auf: useState(0).

React.useState schreiben, ohne React zu importieren

export default function Counter() {
  const [count, setCount] = React.useState(0);

  return <p>Count: {count}</p>;
}

Was React ausgibt

ReferenceError: React is not defined

Warum, und die Lösung

Ältere Beispiele schreiben React.useState. Mit der automatischen JSX-Runtime definiert nichts React für Sie, der Lauf stoppt also mit Uncaught ReferenceError. Importieren Sie den Hook, import { useState } from 'react';, und rufen Sie useState(0) auf.

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

Eine lokale Variable vergisst

Eine Galerie zeigt colours[index], und Next führt index = index + 1 aus. Auf dem Bildschirm passiert nichts, aus zwei Gründen. Eine lokale Variable zu ändern löst kein Rendern aus: React weiß nicht, dass sich etwas geändert hat. Und lokale Variablen überdauern kein Rendern: Rendert React die Komponente aus irgendeinem Grund erneut, läuft let index = 0 wieder und beginnt von vorn. Um sich zu erinnern, braucht eine Komponente zweierlei: Daten, die zwischen Rendervorgängen erhalten bleiben, und einen Weg, React mit den neuen Daten neu rendern zu lassen. useState liefert beides: eine State-Variable, die den Wert behält, und einen Setter, der einen neuen Wert speichert und ein Rendern auslöst.

useState, sein Paar und seine Regeln

const [index, setIndex] = useState(0) bittet React, sich index zu merken. Das Argument ist der Anfangswert, der nur beim ersten Rendern verwendet wird. useState gibt ein Array mit genau zwei Elementen zurück, den aktuellen Wert und seinen Setter; die eckigen Klammern sind Array-Destrukturierung. Nach Konvention heißt das Paar [something, setSomething]. Eine Komponente kann so viele State-Variablen haben, wie sie braucht, etwa index und showMore; React ordnet sie über die Reihenfolge der Aufrufe zu. useState ist ein Hook: eine Funktion, die mit use beginnt und nur verfügbar ist, während React rendert. Rufen Sie Hooks auf der obersten Ebene einer Komponente auf, nie in Bedingungen, Schleifen oder verschachtelten Funktionen.

Jede Instanz hat ihren eigenen privaten State

State gehört zu einer Instanz einer Komponente auf dem Bildschirm, nicht zur Funktion oder zur Datei. Rendern Sie <Gallery /> zweimal, bekommen Sie zwei Indizes: Ein Klick auf Next in der einen lässt die andere unberührt. Eine Variable oben im Modul teilten sich beide, und sie zu ändern würde trotzdem keine der beiden rendern. State ist außerdem privat: Anders als Props kann die Elternkomponente ihn weder lesen noch ändern, und Page weiß nicht einmal, ob Gallery State hat. Um zwei Galerien synchron zu halten, verschieben Sie den State in ihre nächste gemeinsame Elternkomponente, wie in Lektion B1.4.

Quellen

Zuletzt geprüft am 5. Oktober 2026