Zum Inhalt springen
aviral gupta

// B1.4 · ca. 30 Min. · Einstieg

State nach oben verlagern

Nach dieser Lektion hält Ihr Tic-Tac-Toe-Brett seinen State in der Elternkomponente, lässt X und O abwechselnd ziehen und verkündet den Gewinner.

Lektion 4 von 5 in B1 Schnellstart

Danach können Sie

  • Gemeinsamen State in die nächste gemeinsame Elternkomponente verlagern und Wert und Handler als Props weitergeben
  • Ein Array vor der Änderung mit slice() kopieren und dann die Kopie setzen
  • Mit xIsNext abwechselnd ziehen, einen Klick vorzeitig beenden und den Gewinner zeigen
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B1.3: State und Klicks. Sie klicken dreimal auf diesen Button. Was zeigt er?

    function Toggle() {
      const [value, setValue] = useState('X');
    
      function handleClick() {
        if (value === 'X') {
          setValue('O');
        } else {
          setValue('X');
        }
      }
    
      return <button onClick={handleClick}>{value}</button>;
    }
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen. MyApp hält count und gibt es an beide Buttons. Sie klicken zweimal auf den ersten Button. Was zeigen die Buttons?

    function MyButton({ count, onClick }) {
      return <button onClick={onClick}>Clicked {count} times</button>;
    }
    
    function MyApp() {
      const [count, setCount] = useState(0);
    
      function handleClick() {
        setCount(count + 1);
      }
    
      return (
        <div>
          <MyButton count={count} onClick={handleClick} />
          <MyButton count={count} onClick={handleClick} />
        </div>
      );
    }
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den Attributwert so, dass ein Klick auf das Feld squares[0] mit X füllt.

    function Square({ value, onSquareClick }) {
      return <button onClick={onSquareClick}>{value}</button>;
    }
    
    function Board() {
      const [squares, setSquares] = useState([null, null, null]);
    
      function handleClick(i) {
        const nextSquares = squares.slice();
        nextSquares[i] = 'X';
        setSquares(nextSquares);
      }
    
      return <Square value={squares[0]} onSquareClick=____ />;
    }
    onSquareClick= />
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Teil des angehobenen Bretts zu, was es bewirkt.

  5. Üben · Aufgabe 5 von 7

    Reines JavaScript, wie in handleClick. Was gibt das aus?

    const squares = [null, null, null];
    const nextSquares = squares.slice();
    nextSquares[0] = 'X';
    
    console.log(squares[0], nextSquares[0]);
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Dieses handleClick ändert squares selbst und setzt dasselbe Array. Sie klicken auf das erste Feld. Was zeigen die Felder? (_ steht für ein leeres Feld.)

    function Square({ value, onSquareClick }) {
      return <button onClick={onSquareClick}>{value}</button>;
    }
    
    function Board() {
      const [squares, setSquares] = useState([null, null, null]);
    
      function handleClick(i) {
        squares[i] = 'X';
        setSquares(squares);
      }
    
      return (
        <div>
          <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
          <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
          <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
        </div>
      );
    }
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe: zwei Lichtschalter für eine Lampe. Schreiben Sie in main.jsx eine Komponente Switch, die on und onToggle als Props bekommt und einen Button rendert, der On oder Off zeigt. Der Default-Export Room hält on im State, beginnend mit false, und rendert zwei Switches, die ihn teilen: Ein Klick auf einen der beiden schaltet die Lampe um, und beide Buttons zeigen immer dasselbe Wort. Führen Sie es aus und klicken Sie jeden Button.

    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 Brett, das die Felder hält und Züge abwechselt

Das ist das Board aus dem Tutorial nach "Lifting state up" und "Taking turns". Board hält alle neun Werte in squares und in xIsNext, wer am Zug ist. Jedes Square bekommt seinen Wert und eine Funktion onSquareClick. handleClick kehrt bei einem belegten Feld vorzeitig zurück, kopiert das Array mit slice(), schreibt X oder O in die Kopie, setzt sie und dreht xIsNext um. Der Lauf zeigt das Brett vor dem ersten Zug; klicken Sie im Browser Felder an, und X und O wechseln sich ab, der Status folgt.

main.jsx

import { useState } from 'react';

function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    if (squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    setSquares(nextSquares);
    setXIsNext(!xIsNext);
  }

  const status = 'Next player: ' + (xIsNext ? 'X' : 'O');

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

styles.css

.status {
  margin-bottom: 0.5rem;
}

.board-row {
  display: flex;
}

.square {
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 -1px -1px 0;
  border: 1px solid #999;
  background: #fff;
  font-size: 1.5rem;
  font-weight: bold;
}

Ausführen mit

npm run dev

Ausgabe

<!-- #root -->
<div id="root"><div class="status">Next player: X</div><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div></div>
  • Die Statuszeile wird bei jedem Rendern aus xIsNext berechnet; sie ist kein eigener State.
  • Alle neun Buttons sind leer, weil jeder Eintrag von Array(9).fill(null) null ist.
  • Weder onClick noch onSquareClick erscheinen im HTML: React hängt die Handler selbst an.
  • Den Gewinner zu finden ist Übung 2; einen Verlauf der Züge zu führen ist Lektion B1.5.
Ä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 Felder in Board anheben

Der Starter ist das Brett aus Lektion B1.3: Jedes Square hält seinen eigenen Wert. Verlagern Sie den State nach oben. Board deklariert squares, beginnend mit Array(9).fill(null), und ein handleClick(i), das das Array mit slice() kopiert, an Stelle i X schreibt und die Kopie setzt. Square hat keinen State mehr: Es bekommt value und onSquareClick als Props, zeigt value und ruft beim Klick onSquareClick auf. Board gibt jedem Square squares[i] und () => handleClick(i). Lassen Sie export vor Square stehen: Die Tests rendern es auch allein.

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 in Square State und Handler durch Props: function Square({ value, onSquareClick }) und onClick={onSquareClick}.

  2. Hinweis 2

    Oben in Board: const [squares, setSquares] = useState(Array(9).fill(null));

  3. Hinweis 3

    Jedes Tag wird zu <Square value={squares[0]} onSquareClick={() => handleClick(0)} />, mit 0 bis 8.

Eine Lösung zeigen

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

import { useState } from 'react';

export function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    const nextSquares = squares.slice();
    nextSquares[i] = 'X';
    setSquares(nextSquares);
  }

  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}
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';

export function Square() {
  // TODO: take value and onSquareClick as props instead of keeping state here.
  const [value, setValue] = useState(null);

  function handleClick() {
    setValue('X');
  }

  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}

export default function Board() {
  // TODO: keep all nine values in a squares state here, and pass each Square its value and a click handler.
  return (
    <>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </>
  );
}

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 Board, {Square} from './main.jsx';

const labels = () => screen.getAllByRole('button').map((button) => button.textContent);

test('Alle neun Felder sind anfangs leer', () => {
  render(<Board />);
  assert.deepEqual(labels(), ['', '', '', '', '', '', '', '', ''], 'vor dem ersten Klick zeigen die Felder ' + JSON.stringify(labels()));
});

test('Square zeigt das Prop value, das es bekommt', () => {
  render(<Square value="O" onSquareClick={() => {}} />);
  const label = screen.getByRole('button').textContent;
  assert.equal(label, 'O', 'Square mit value="O" zeigt ' + JSON.stringify(label));
});

test('Square ruft beim Klick onSquareClick auf', async () => {
  const user = userEvent.setup();
  let calls = 0;
  render(<Square value={null} onSquareClick={() => calls++} />);
  await user.click(screen.getByRole('button'));
  assert.equal(calls, 1, 'ein Klick hat onSquareClick ' + calls + '-mal aufgerufen');
});

test('Klicks auf Felder in Board füllen sie mit X', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await user.click(screen.getAllByRole('button')[4]);
  await user.click(screen.getAllByRole('button')[0]);
  assert.deepEqual(labels(), ['X', '', '', '', 'X', '', '', '', ''], 'nach Klicks auf das mittlere und das erste Feld zeigen die Felder ' + JSON.stringify(labels()));
});

test('React gibt keine Warnungen aus', async () => {
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    const user = userEvent.setup();
    render(<Board />);
    await user.click(screen.getAllByRole('button')[0]);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error beim Rendern und Klicken');
});

styles.css

.status {
  margin-bottom: 0.5rem;
}

.board-row {
  display: flex;
}

.square {
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 -1px -1px 0;
  border: 1px solid #999;
  background: #fff;
  font-size: 1.5rem;
  font-weight: bold;
}

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

Bei belegten Feldern stoppen und den Gewinner verkünden

Dieses Brett wechselt die Züge schon ab, aber ein Klick auf ein belegtes Feld überschreibt es, und niemand gewinnt je. calculateWinner steht am Ende der Datei; es gibt X, O oder null zurück. Lassen Sie handleClick vorzeitig zurückkehren, wenn squares[i] belegt ist oder calculateWinner(squares) einen Gewinner findet. Zeigen Sie dann als Status Winner: X (oder O), sobald es einen Gewinner gibt, und sonst Next player: X oder O.

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ühren Sie es aus und klicken Sie ein Feld zweimal an: Aus dem X wird ein O.

  2. Hinweis 2

    Erste Zeile von handleClick: if (squares[i] || calculateWinner(squares)) { return; }

  3. Hinweis 3

    Vor dem return: const winner = calculateWinner(squares); dann setzen Sie status auf 'Winner: ' + winner, wenn es einen gibt.

Eine Lösung zeigen

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

import { useState } from 'react';

function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    setSquares(nextSquares);
    setXIsNext(!xIsNext);
  }

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}
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';

function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    // TODO: return early if the square is filled or someone has won.
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    setSquares(nextSquares);
    setXIsNext(!xIsNext);
  }

  // TODO: show "Winner: X" or "Winner: O" once calculateWinner finds one.
  const status = 'Next player: ' + (xIsNext ? 'X' : 'O');

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

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

const labels = () => screen.getAllByRole('button').map((button) => button.textContent);
const status = () => screen.getByText(/^(Next player|Winner):/).textContent;
const play = async (user, squares) => {
  for (const i of squares) await user.click(screen.getAllByRole('button')[i]);
};

test('X und O ziehen abwechselnd', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await play(user, [0, 1, 2]);
  assert.deepEqual(labels().slice(0, 3), ['X', 'O', 'X'], 'nach drei Zügen zeigt die obere Reihe ' + JSON.stringify(labels().slice(0, 3)));
  assert.equal(status(), 'Next player: O', 'nach drei Zügen zeigt der Status ' + JSON.stringify(status()));
});

test('Ein belegtes Feld ändert sich nicht', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await play(user, [4, 4]);
  assert.equal(labels()[4], 'X', 'nach zwei Klicks auf das mittlere Feld zeigt es ' + JSON.stringify(labels()[4]));
  assert.equal(status(), 'Next player: O', 'ein Klick auf ein belegtes Feld darf den Zug nicht weitergeben; der Status zeigt ' + JSON.stringify(status()));
});

test('Drei in einer Reihe zeigen den Gewinner', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await play(user, [0, 3, 1, 4, 2]);
  assert.equal(status(), 'Winner: X', 'nachdem X die obere Reihe gefüllt hat, zeigt der Status ' + JSON.stringify(status()));
});

test('Nach einem Sieg keine Züge mehr', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await play(user, [0, 3, 1, 4, 2, 8]);
  assert.equal(labels()[8], '', 'nach dem Sieg zeigt das letzte Feld nach einem Klick ' + JSON.stringify(labels()[8]));
});

test('React gibt keine Warnungen aus', async () => {
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    const user = userEvent.setup();
    render(<Board />);
    await user.click(screen.getAllByRole('button')[0]);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error beim Rendern und Klicken');
});

styles.css

.status {
  margin-bottom: 0.5rem;
}

.board-row {
  display: flex;
}

.square {
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 -1px -1px 0;
  border: 1px solid #999;
  background: #fff;
  font-size: 1.5rem;
  font-weight: bold;
}

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

Den angehobenen State mit null beginnen

import { useState } from 'react';

function Square({ value, onSquareClick }) {
  return <button onClick={onSquareClick}>{value}</button>;
}

export default function Board() {
  const [squares, setSquares] = useState(null);

  return <Square value={squares[0]} onSquareClick={() => {}} />;
}

Was React ausgibt

TypeError: Cannot read properties of null (reading '0')

Warum, und die Lösung

useState(null) passte für ein einzelnes Square; Board braucht einen Eintrag pro Feld. So liest squares[0] aus null, und das erste Rendern bricht mit Uncaught TypeError ab. Beginnen Sie mit useState(Array(9).fill(null)). Firefox und Safari formulieren diesen TypeError anders; der zitierte Text ist der von V8 (Chrome, Node.js).

Das Handler-Prop verwenden, ohne es entgegenzunehmen

import { useState } from 'react';

function Square({ value }) {
  return <button onClick={onSquareClick}>{value}</button>;
}

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    const nextSquares = squares.slice();
    nextSquares[i] = 'X';
    setSquares(nextSquares);
  }

  return <Square value={squares[0]} onSquareClick={() => handleClick(0)} />;
}

Was React ausgibt

ReferenceError: onSquareClick is not defined

Warum, und die Lösung

Board übergibt onSquareClick, aber Square nimmt nur value aus seinen Props. Der Name onSquareClick existiert in Square also nicht, und das erste Rendern bricht mit Uncaught ReferenceError ab. Ergänzen Sie ihn in den geschweiften Klammern: function Square({ value, onSquareClick }).

calculateWinner ohne die Felder aufrufen

import { useState } from 'react';

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  const winner = calculateWinner();

  return <div className="status">{winner ? 'Winner: ' + winner : 'Next player: X'}</div>;
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

Was React ausgibt

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

Warum, und die Lösung

calculateWinner ist eine gewöhnliche Funktion: Sie sieht nur das Array, das Sie ihr übergeben. Ohne Argument ist ihr Parameter squares undefined, und die erste Prüfung, squares[a] mit a = 0, bricht das Rendern mit Uncaught TypeError ab. Übergeben Sie den State: calculateWinner(squares). Auch hier ist es der Wortlaut von V8; Firefox und Safari formulieren es anders.

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

Gemeinsamen State in die nächste gemeinsame Elternkomponente heben

Brauchen zwei Komponenten dieselben Daten, oder muss eine Elternkomponente wissen, was ihre Kinder zeigen, halten Sie den State in der nächsten gemeinsamen Elternkomponente statt in jedem Kind. Um einen Gewinner zu finden, braucht Board alle neun Werte. Board deklariert also const [squares, setSquares] = useState(Array(9).fill(null)) und gibt jedem Square seinen Wert als Prop: <Square value={squares[0]} … />. Square hat keinen eigenen State mehr; es zeigt, was Board ihm gibt. Ändert sich der State von Board, rendern Board und alle seine Squares mit den neuen Werten neu.

Einen Handler weitergeben; das Kind ruft ihn auf

State gehört allein der Komponente, die ihn deklariert, deshalb kann Square setSquares nicht aufrufen. Board gibt stattdessen eine Funktion weiter, nach Konvention onEtwas genannt: onSquareClick={() => handleClick(0)}. Square setzt sie auf seinen Button, onClick={onSquareClick}, React ruft sie beim Klick auf, und handleClick in Board ändert den State. Die Pfeilfunktion ist wichtig: onSquareClick={handleClick(0)} würde handleClick schon aufrufen, während Board rendert. Der Quick Start macht dasselbe mit einem Zähler: MyApp hält count und gibt count und sein handleClick an beide Buttons, die sich so gemeinsam aktualisieren.

Erst kopieren, dann setzen; abwechselnd ziehen, früh aufhören

handleClick ändert squares nie selbst. Es kopiert das Array mit squares.slice(), ändert die Kopie und übergibt sie an setSquares. Ändern Sie squares direkt und rufen setSquares(squares) auf, sieht React dasselbe Array und rendert nicht neu. Kopien lassen außerdem alte Bretter unverändert, was Lektion B1.5 für den Verlauf braucht. Ein zweiter State, xIsNext, sagt, wer am Zug ist, und setXIsNext(!xIsNext) dreht ihn nach jedem Zug um. handleClick kehrt vorzeitig zurück, wenn das Feld belegt ist oder calculateWinner(squares), eine gewöhnliche JavaScript-Funktion, einen Gewinner gefunden hat.

Quellen

Zuletzt geprüft am 5. Oktober 2026