Zum Inhalt springen
aviral gupta

// B1.5 · ca. 40 Min. · Einstieg

Projekt: Tic-Tac-Toe mit Verlauf

In diesem Projekt stellen Sie das Tic-Tac-Toe-Spiel fertig: Game speichert jedes Brett in einem Verlauf, listet die Züge auf und lässt die Spieler in der Zeit zurückspringen.

Lektion 5 von 5 in B1 Schnellstart

Ende des Moduls

Danach können Sie

  • Den State als Verlauf von Brettern in Game heben und Board über die Props xIsNext, squares und onPlay steuern
  • Die Zugliste mit map rendern und jedem Eintrag einen Key geben
  • Zu einem früheren Zug springen, beim Weiterspielen die späteren Züge verwerfen und xIsNext aus currentMove berechnen
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B1.4: angehobener State und Spielerwechsel. Sie klicken auf das dritte Feld, dann auf das erste. Was zeigen die Felder? (_ steht für ein leeres Feld.)

    function Square({ value, onSquareClick }) {
      return <button onClick={onSquareClick}>{value}</button>;
    }
    
    function Board() {
      const [xIsNext, setXIsNext] = useState(true);
      const [squares, setSquares] = useState([null, null, null]);
    
      function handleClick(i) {
        if (squares[i]) {
          return;
        }
        const nextSquares = squares.slice();
        nextSquares[i] = xIsNext ? 'X' : 'O';
        setSquares(nextSquares);
        setXIsNext(!xIsNext);
      }
    
      return (
        <div>
          <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
          <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
          <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
        </div>
      );
    }
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen. Game wird jedes Brett in einem Array namens history speichern. Was gibt das aus?

    const history = [[null, null, null]];
    const nextSquares = ['X', null, null];
    const nextHistory = [...history, nextSquares];
    
    console.log(history.length, nextHistory.length, nextHistory[0][0]);
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den Index so, dass Game das Brett des gewählten Zugs zeigt, nicht immer das letzte.

    const currentSquares = history[____];
    const currentSquares = history[];
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Teil des fertigen Spiels zu, was es bewirkt.

  5. Üben · Aufgabe 5 von 7

    history enthält drei Bretter: den leeren Anfang und zwei Züge. Was steht auf den Buttons?

    function Moves() {
      const history = [
        [null, null],
        ['X', null],
        ['X', 'O']
      ];
    
      const moves = history.map((squares, move) => {
        let description;
        if (move > 0) {
          description = 'Go to move #' + move;
        } else {
          description = 'Go to game start';
        }
        return (
          <li key={move}>
            <button>{description}</button>
          </li>
        );
      });
    
      return <ol>{moves}</ol>;
    }
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Die Spieler sind zu Zug 1 zurückgegangen und machen jetzt einen neuen Zug. Was gibt das aus?

    const history = ['start', 'move 1', 'move 2', 'move 3'];
    const currentMove = 1;
    const nextSquares = 'new move';
    
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    console.log(nextHistory.join(' | '));
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe, die erste Zusatzaufgabe am Ende des Tutorials auf react.dev: Zeigen Sie nur für den aktuellen Zug "You are at move #…" als Text statt eines Buttons. Beginnen Sie mit dem Beispiel. Vergleichen Sie im map-Callback move mit currentMove und geben Sie für den aktuellen Zug ein anderes <li> zurück, mit demselben Key. Führen Sie es aus, spielen Sie ein paar Züge und springen Sie zurück: Der Text folgt dem Zug, bei dem Sie sind.

    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

Das fertige Spiel mit Zeitreise

Das ist der fertige Code aus dem Tutorial. Game hält history und currentMove, berechnet daraus xIsNext und currentSquares und gibt xIsNext, squares und onPlay an Board. handlePlay schneidet den Verlauf beim aktuellen Zug ab, bevor es das neue Brett anhängt. Die Zugliste ist history.map(…) mit key={move}. Der Lauf zeigt das Spiel vor dem ersten Zug: ein leeres Brett und einen Button. Spielen Sie im Browser ein paar Züge und klicken Sie dann Go to game start oder einen Zug an.

main.jsx

import { useState } from 'react';

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

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

  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>
    </>
  );
}

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = 'Go to move #' + move;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </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;
}

styles.css

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.status {
  margin-bottom: 10px;
}

.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="game"><div class="game-board"><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><div class="game-info"><ol><li><button>Go to game start</button></li></ol></div></div></div>
  • history beginnt mit einem leeren Brett, die Liste hat also einen Button.
  • Das Fragment von Board hinterlässt kein Element: Status und Reihen stehen direkt in game-board.
  • key erscheint nicht im HTML; React behält es für sich.
  • Vor dem Snapshot wurde nichts ausgegeben, React hat also nicht vor fehlenden Keys gewarnt.
Ä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 3

Den State in Game heben

Der Starter ist das Spiel aus Lektion B1.4, umgeben von einem neuen Default-Export, Game. Verlagern Sie den State nach oben. Game hält xIsNext (Start true) und history (Start [Array(9).fill(null)]); das aktuelle Brett ist history[history.length - 1]. handlePlay(nextSquares) hängt das Brett mit setHistory([...history, nextSquares]) an und dreht xIsNext um. Board bekommt xIsNext, squares und onPlay als Props, hat kein useState mehr und ruft statt seiner Setter onPlay(nextSquares) auf. Lassen Sie export vor Board 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

    Board beginnt so: export function Board({ xIsNext, squares, onPlay }) {, ohne die beiden useState-Zeilen.

  2. Hinweis 2

    Ersetzen Sie in handleClick von Board setSquares(nextSquares) und setXIsNext(!xIsNext) durch onPlay(nextSquares).

  3. Hinweis 3

    In Game: <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />

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 function Board({ xIsNext, squares, onPlay }) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    onPlay(nextSquares);
  }

  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>
    </>
  );
}

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

  function handlePlay(nextSquares) {
    setHistory([...history, nextSquares]);
    setXIsNext(!xIsNext);
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{/* the move list comes in exercise 2 */}</ol>
      </div>
    </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 function Board() {
  // TODO: take xIsNext, squares and onPlay as props instead of keeping state here.
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      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>
    </>
  );
}

export default function Game() {
  // TODO: keep xIsNext and history (a list of squares arrays) here, and pass
  // xIsNext, the latest squares and handlePlay down to Board.
  return (
    <div className="game">
      <div className="game-board">
        <Board />
      </div>
      <div className="game-info">
        <ol>{/* the move list comes in exercise 2 */}</ol>
      </div>
    </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 Game, {Board} from './main.jsx';

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

test('Das Spiel läuft weiter: X gewinnt die obere Reihe', async () => {
  const user = userEvent.setup();
  render(<Game />);
  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('Board zeigt das Prop squares', () => {
  render(<Board xIsNext={true} squares={['X', 'O', null, null, null, null, null, null, null]} onPlay={() => {}} />);
  assert.deepEqual(squares().slice(0, 3), ['X', 'O', ''], 'mit squares X, O, null zeigt die erste Reihe ' + JSON.stringify(squares().slice(0, 3)));
});

test('Board zeigt anhand des Props xIsNext, wer am Zug ist', () => {
  render(<Board xIsNext={false} squares={EMPTY} onPlay={() => {}} />);
  assert.equal(status(), 'Next player: O', 'mit xIsNext={false} zeigt der Status ' + JSON.stringify(status()));
});

test('Board ruft onPlay mit den nächsten Feldern auf', async () => {
  const user = userEvent.setup();
  let played = null;
  render(<Board xIsNext={true} squares={EMPTY} onPlay={(next) => { played = next; }} />);
  await play(user, [4]);
  assert.deepEqual(played, [null, null, null, null, 'X', null, null, null, null], 'ein Klick auf das mittlere Feld rief onPlay auf mit ' + JSON.stringify(played));
});

test('Board hat keinen eigenen State', async () => {
  const user = userEvent.setup();
  render(<Board xIsNext={true} squares={EMPTY} onPlay={() => {}} />);
  await play(user, [0]);
  assert.equal(squares()[0], '', 'wenn onPlay den Zug ignoriert, muss das angeklickte Feld leer bleiben; es zeigt ' + JSON.stringify(squares()[0]));
});

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(<Game />);
    await play(user, [0, 4]);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error beim Rendern und Spielen');
});

styles.css

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.status {
  margin-bottom: 10px;
}

.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 3

Frühere Züge zeigen und zurückreisen

Bauen Sie die Zeitreise ein. Game bekommt einen zweiten State, currentMove, beginnend bei 0, und zeigt history[currentMove]. Machen Sie mit map aus history die Zugliste: ein <li key={move}> pro Brett, mit einem Button, der für Zug 0 Go to game start und für die anderen Go to move #1, #2 … sagt und jumpTo(move) aufruft. jumpTo setzt currentMove und setzt xIsNext auf nextMove % 2 === 0. handlePlay behält nur history.slice(0, currentMove + 1), bevor es das neue Brett anhängt, und setzt currentMove auf den neuen letzten Index.

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

    const moves = history.map((squares, move) => { … return <li key={move}><button onClick={() => jumpTo(move)}>{description}</button></li>; }); dann <ol>{moves}</ol>.

  2. Hinweis 2

    function jumpTo(nextMove) { setCurrentMove(nextMove); setXIsNext(nextMove % 2 === 0); }

  3. Hinweis 3

    In handlePlay: const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1);

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>
  );
}

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

  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>
    </>
  );
}

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const currentSquares = history[currentMove];

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
    setXIsNext(!xIsNext);
  }

  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
    setXIsNext(nextMove % 2 === 0);
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = 'Go to move #' + move;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </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>
  );
}

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

  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>
    </>
  );
}

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  // TODO: add currentMove, and show history[currentMove] instead of the last board.
  const currentSquares = history[history.length - 1];

  function handlePlay(nextSquares) {
    // TODO: keep only history.slice(0, currentMove + 1) before adding nextSquares,
    // and point currentMove at the new last entry.
    setHistory([...history, nextSquares]);
    setXIsNext(!xIsNext);
  }

  // TODO: write jumpTo(nextMove), and turn history into one <li> with a button per entry.

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{/* TODO: the moves */}</ol>
      </div>
    </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, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Game from './main.jsx';

const squares = () => screen.getAllByRole('button').slice(0, 9).map((button) => button.textContent);
const status = () => screen.getByText(/^(Next player|Winner):/).textContent;
const play = async (user, positions) => {
  for (const i of positions) await user.click(screen.getAllByRole('button')[i]);
};
const moves = () => within(screen.getByRole('list')).queryAllByRole('button').map((button) => button.textContent);
const jump = async (user, name) => user.click(within(screen.getByRole('list')).getByRole('button', {name}));

test('Die Liste beginnt mit Go to game start', () => {
  render(<Game />);
  assert.deepEqual(moves(), ['Go to game start'], 'vor dem ersten Zug zeigt die Liste ' + JSON.stringify(moves()));
});

test('Jeder Zug fügt einen Button hinzu', async () => {
  const user = userEvent.setup();
  render(<Game />);
  await play(user, [0, 1]);
  assert.deepEqual(moves(), ['Go to game start', 'Go to move #1', 'Go to move #2'], 'nach zwei Zügen zeigt die Liste ' + JSON.stringify(moves()));
});

test('Go to game start zeigt das leere Brett', async () => {
  const user = userEvent.setup();
  render(<Game />);
  await play(user, [0, 1]);
  await jump(user, 'Go to game start');
  assert.deepEqual(squares(), ['', '', '', '', '', '', '', '', ''], 'nach dem Sprung zum Anfang zeigen die Felder ' + JSON.stringify(squares()));
  assert.equal(status(), 'Next player: X', 'nach dem Sprung zum Anfang zeigt der Status ' + JSON.stringify(status()));
});

test('Go to move #1 zeigt das Brett nach dem ersten Zug', async () => {
  const user = userEvent.setup();
  render(<Game />);
  await play(user, [0, 1, 2]);
  await jump(user, 'Go to move #1');
  assert.deepEqual(squares().slice(0, 3), ['X', '', ''], 'nach dem Sprung zu Zug #1 zeigt die obere Reihe ' + JSON.stringify(squares().slice(0, 3)));
  assert.equal(status(), 'Next player: O', 'nach dem Sprung zu Zug #1 zeigt der Status ' + JSON.stringify(status()));
});

test('Ein Zug nach einem Sprung ersetzt die späteren Züge', async () => {
  const user = userEvent.setup();
  render(<Game />);
  await play(user, [0, 1, 2]);
  await jump(user, 'Go to move #1');
  await play(user, [4]);
  assert.deepEqual(moves(), ['Go to game start', 'Go to move #1', 'Go to move #2'], 'nach dem Sprung zu Zug #1 und einem neuen Zug zeigt die Liste ' + JSON.stringify(moves()));
  assert.deepEqual(squares().slice(0, 5), ['X', '', '', '', 'O'], 'das Brett zeigt ' + JSON.stringify(squares().slice(0, 5)) + ' in seinen ersten fünf Feldern');
});

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(<Game />);
    await play(user, [0, 4]);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error beim Rendern und Spielen');
});

styles.css

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.status {
  margin-bottom: 10px;
}

.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 3 von 3

Den doppelten State entfernen

Jemand hat in jumpTo die Zeile gelöscht, die xIsNext setzte, und jetzt stimmt der Zug nach einem Sprung nicht: Nach zwei Zügen und einem Sprung zu Zug #1 sagt der Status, X sei am Zug, obwohl O dran ist. Setzen Sie die Zeile nicht wieder ein. xIsNext ist genau dann true, wenn currentMove gerade ist. Entfernen Sie also sein useState und jeden Aufruf von setXIsNext und berechnen Sie es stattdessen beim Rendern.

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: Spielen Sie zwei Züge, klicken Sie dann Go to move #1 an und lesen Sie den Status.

  2. Hinweis 2

    Löschen Sie const [xIsNext, setXIsNext] = useState(true); und die Zeile setXIsNext(!xIsNext) in handlePlay.

  3. Hinweis 3

    Ergänzen Sie nach dem useState für currentMove: const xIsNext = currentMove % 2 === 0;

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>
  );
}

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

  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>
    </>
  );
}

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = 'Go to move #' + move;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </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>
  );
}

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

  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>
    </>
  );
}

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const currentSquares = history[currentMove];

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
    setXIsNext(!xIsNext);
  }

  function jumpTo(nextMove) {
    // TODO: xIsNext follows from currentMove; remove its state instead of setting it here.
    setCurrentMove(nextMove);
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = 'Go to move #' + move;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </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, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Game from './main.jsx';

const squares = () => screen.getAllByRole('button').slice(0, 9).map((button) => button.textContent);
const status = () => screen.getByText(/^(Next player|Winner):/).textContent;
const play = async (user, positions) => {
  for (const i of positions) await user.click(screen.getAllByRole('button')[i]);
};
const moves = () => within(screen.getByRole('list')).queryAllByRole('button').map((button) => button.textContent);
const jump = async (user, name) => user.click(within(screen.getByRole('list')).getByRole('button', {name}));

test('Nach einem Sprung zum Anfang ist X am Zug', async () => {
  const user = userEvent.setup();
  render(<Game />);
  await play(user, [0]);
  await jump(user, 'Go to game start');
  assert.equal(status(), 'Next player: X', 'nach einem Zug und einem Sprung zum Anfang zeigt der Status ' + JSON.stringify(status()));
});

test('Nach einem Sprung zu Zug #1 ist O am Zug', async () => {
  const user = userEvent.setup();
  render(<Game />);
  await play(user, [0, 1]);
  await jump(user, 'Go to move #1');
  assert.equal(status(), 'Next player: O', 'nach zwei Zügen und einem Sprung zu Zug #1 zeigt der Status ' + JSON.stringify(status()));
});

test('Der Zug nach einem Sprung setzt das richtige Zeichen', async () => {
  const user = userEvent.setup();
  render(<Game />);
  await play(user, [0, 1]);
  await jump(user, 'Go to move #1');
  await play(user, [4]);
  assert.equal(squares()[4], 'O', 'nach einem Sprung zu Zug #1 setzte der nächste Zug ' + JSON.stringify(squares()[4]));
});

test('Ein ganzes Spiel findet weiterhin den Gewinner', async () => {
  const user = userEvent.setup();
  render(<Game />);
  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()));
  assert.equal(moves().length, 6, 'nach fünf Zügen enthält die Liste ' + moves().length + ' Buttons');
});

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(<Game />);
    await play(user, [0, 4]);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error beim Rendern und Spielen');
});

styles.css

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.status {
  margin-bottom: 10px;
}

.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

Eine Zugliste ohne Keys

export default function Moves() {
  const history = [[null], ['X'], ['X', 'O']];

  const moves = history.map((squares, move) => (
    <li>
      <button>Go to move #{move}</button>
    </li>
  ));

  return <ol>{moves}</ol>;
}

Was React ausgibt

Each child in a list should have a unique "key" prop.

Warum, und die Lösung

Die Liste wird trotzdem gerendert, aber React warnt, und der Runner des Kurses wertet jede Warnung als gescheiterten Lauf. Geben Sie dem äußersten Element, das der Callback zurückgibt, einen Key: <li key={move}>. Das Tutorial zeigt einen älteren Wortlaut derselben Warnung ("Each child in an array or iterator…"); React 19 gibt den hier zitierten Satz aus.

Den Key in Anführungszeichen schreiben

export default function Moves() {
  const history = [[null], ['X'], ['X', 'O']];

  const moves = history.map((squares, move) => (
    <li key="move">
      <button>Go to move #{move}</button>
    </li>
  ));

  return <ol>{moves}</ol>;
}

Was React ausgibt

Encountered two children with the same key, `move`.

Warum, und die Lösung

key="move" gibt jedem Eintrag denselben Text, move. Die Keys sind also nicht mehr eindeutig, und React warnt. Übergeben Sie die Variable mit geschweiften Klammern: key={move}.

handlePlay aufrufen, wo Board eine Funktion braucht

import { useState } from 'react';

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

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

  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>
    </>
  );
}

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

  function handlePlay(nextSquares) {
    setHistory([...history, nextSquares]);
  }

  return <Board xIsNext={true} squares={currentSquares} onPlay={handlePlay(nextSquares)} />;
}

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

ReferenceError: nextSquares is not defined

Warum, und die Lösung

onPlay={handlePlay(nextSquares)} versucht, handlePlay schon beim Rendern von Game aufzurufen, und nextSquares existiert nur in handleClick von Board. Das erste Rendern bricht also mit Uncaught ReferenceError ab. Übergeben Sie die Funktion selbst, onPlay={handlePlay}; Board ruft sie bei einem Zug mit dem neuen Brett 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

Den State noch einmal anheben, in Game

Um in der Zeit zurückzugehen, muss sich etwas jedes Brett merken, nicht nur das aktuelle. Game, die neue oberste Komponente, hält history: ein Array von squares-Arrays, beginnend mit [Array(9).fill(null)]. Board verliert seine useState-Aufrufe und bekommt drei Props: xIsNext, squares und onPlay. Bei einem Zug kopiert Board squares, schreibt X oder O in die Kopie und ruft onPlay(nextSquares) auf; handlePlay in Game hängt sie mit setHistory([...history, nextSquares]) an. Board wird jetzt ganz von Game gesteuert: Es zeigt nur, was es bekommt. Das klappt, weil jeder Zug eine Kopie angelegt hat; die alten Bretter sind also unverändert.

Eine Liste der Züge, jeder mit Key

history.map((squares, move) => …) macht aus den Brettern Listeneinträge: Der Button für Zug 0 sagt Go to game start, die anderen Go to move #1, #2 und so weiter. React braucht an jedem Eintrag einer Liste einen Key, um die Einträge auseinanderzuhalten, wenn sich die Liste ändert. Ohne Key warnt es: Each child in a list should have a unique "key" prop. Hier ist key={move} sicher, weil Züge nie umsortiert, gelöscht oder in der Mitte eingefügt werden. key ist besonders: React liest es, und die Komponente bekommt es nie als Prop. Lektion B2.5 behandelt Keys ausführlich.

Zeitreise und ein State weniger

Ein zweiter State, currentMove, sagt, welches Brett angezeigt wird: currentSquares = history[currentMove]. Jeder Button ruft jumpTo(move) auf, das currentMove setzt. Zieht jemand nach einem Sprung zurück, gehören die späteren Züge nicht mehr zum Spiel. handlePlay behält deshalb nur history.slice(0, currentMove + 1), hängt das neue Brett an und setzt currentMove darauf. Zuletzt: xIsNext ist genau dann true, wenn currentMove gerade ist, und braucht keinen eigenen State: const xIsNext = currentMove % 2 === 0. Doppelter State kann auseinanderlaufen; ein beim Rendern berechneter Wert nicht.

Quellen

Zuletzt geprüft am 5. Oktober 2026