skip to Main Content

Following along with the react tutorial to create a tic-tac-toe game, and it works, no problem there. I get why nearly all of it works, apart from the display of the winner.

import { useState } from 'react';

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

    function handleClick(idx) {
        const nextSquares = squares.slice();

        if (nextSquares[idx] || calculateWinner(squares)) {
            return;
        }

        nextSquares[idx] = xIsNext ? 'X' : '0';

        setSquares(nextSquares);
        setXIsNext(!xIsNext);
    }

    let winner = calculateWinner(squares);
    let status = winner
        ? 'Winner: ' + winner
        : 'Next player: ' + (xIsNext ? 'X' : '0');

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

The squares updating is clear – the state is set and the Square component is updated.

What I don’t get is how status is recalculated. What triggers that little block of code to set winner and status to run again? Is it updating the state? If that is run again, then the definition of xIsNext and friends must be happening again?

There is some magic there that I’m not getting and it isn’t explained in the tutorial (which otherwise is really clear).

Many thanks for any insight.

3

Answers


  1. What triggers that little block of code to set winner and status to run again? Is it updating the state?

    Yes. Updates to state trigger a re-render, and a re-render executes the entire Board() function again. Functionality in React’s hooks (useState, useEffect, etc.) internally manages what it does on subsequent renders/executions, because they are executed on every render.

    Simple assignment statements in the body of the component are assigned on every render.


    Incidentally, this can matter significantly if logic within the component involves heavy processing/calculations/etc. If it’s surprising to a developer that the entire component re-executes, potentially many times, then this could lead to significant performance problems. It’s good to make use of things like useCallback and useMemo, etc. to mitigate this.

    Login or Signup to reply.
  2. In React, the status calculation and update are part of the component’s rendering cycle. Here’s a breakdown of how this works:

    Component Rendering and State Updates:

    When you call setSquares(nextSquares) or setXIsNext(!xIsNext), React schedules a re-render of the Board component. This means the component function (i.e., Board) is called again, with the updated state values.

    Recalculation of status:

    During each render, the status and winner variables are recalculated based on the current state values (squares and xIsNext).
    The line let winner = calculateWinner(squares); runs every time the Board component renders. Similarly, let status = winner ? ‘Winner: ‘ + winner : ‘Next player: ‘ + (xIsNext ? ‘X’ : ‘0’); is recalculated with each render.

    React’s Re-rendering:

    When you update the state with setSquares or setXIsNext, React triggers a re-render of the Board component.
    During this re-render, React calls the Board function again. This causes the winner and status variables to be recalculated based on the updated state.

    You’re seeing is the way React handles state changes and re-renders. Each time the state updates, React re-executes the Board function, so status is recalculated with the new state.
    In summary, every time state changes, React re-renders the component, running the Board function again and recalculating status and winner with the updated state. This is why the displayed status is always current with the latest state.

    Login or Signup to reply.
  3. What I don’t get is how status is recalculated. What triggers that little block of code to set winner and status to run again?

    • winner and status are being calculated every time a re-render is triggered.

    Is it updating the state?

    • Yes

    If that is run again, then the definition of xIsNext and friends must be happening again?

    • No, it’s not happening again. Their values are saved untill the next set of the value is invoked.

    "any insight"

    • The winner calculation can happen on player turn instead of every rerender.
    • Here is a little different approach to the task:

    .

    import { useState } from 'react';
    
    const statuses = {
      IN_PROGRESS: 'IN_PROGRESS',
      FINISHED: 'FINISHED'
    };
    
    const players = {
      X: 'X',
      O: 'O'
    };
    
    const getNewBoard = () => {
      return [
        [null, null, null],
        [null, null, null],
        [null, null, null]
      ];
    };
    
    const calculateWinner = (board = []) => {
      // check every row and column
      for (let i = 0; i < 3; i++) {
        const rowCheck = [...new Set(board[i])];
        if (rowCheck.length === 1 && rowCheck[0] !== null) {
          return rowCheck[0];
        }
    
        const colCheck = [...new Set(board.reduce((col, row) => [...col, row[i]], []))];
        if (colCheck.length === 1 && colCheck[0] !== null) {
          return colCheck[0];
        }
      }
    
      // check main diagonal
      const mainDiag = [...new Set([board[0][0], board[1][1], board[2][2]])];
      if (mainDiag.length === 1 && mainDiag[0] !== null) {
        return mainDiag[0];
      }
    
      // check secondary diagonal
      const secDiag = [...new Set([board[0][2], board[1][1], board[2][0]])];
      if (secDiag.length === 1 && secDiag[0] !== null) {
        return secDiag[0];
      }
    };
    
    const Board = () => {
      const [board, setBoard] = useState(getNewBoard());
      const [status, setStatus] = useState(statuses.IN_PROGRESS);
      const [playerTurn, setPlayerTurn] = useState(players.X);
      const [winner, setWinner] = useState(null);
    
      const startNewGame = () => {
        setBoard(getNewBoard());
        setStatus(statuses.IN_PROGRESS);
        setPlayerTurn(players.X);
        setWinner(null);
      };
    
      const makeTurn = (row, col) => {
        if (status === statuses.FINISHED || board[row][col]) {
          return;
        }
    
        const newBoard = board.map((row) => row.slice());
        newBoard[row][col] = playerTurn;
        const winner = calculateWinner(newBoard);
    
        setBoard(newBoard);
    
        if (winner) {
          setStatus(statuses.FINISHED);
          setWinner(winner);
          return;
        }
    
        setPlayerTurn(playerTurn === players.X ? players.O : players.X);
      };
    
      return (
        <>
          <div className="status">
            {status === statuses.IN_PROGRESS && `Next player: ${playerTurn}`}
            {status === statuses.FINISHED && `Winner: ${winner}`}
          </div>
    
          {board.map((row, rowNum) => {
            return (
              <div className="board-row" key={rowNum}>
                {row.map((value, colNum) => {
                  return (
                    <Square key={colNum} val={value} onSquareClick={() => makeTurn(rowNum, colNum)} />
                  );
                })}
              </div>
            );
          })}
    
          <div>
            <button onClick={startNewGame}>
              {status === statuses.IN_PROGRESS ? 'Restart game' : 'New game'}
            </button>
          </div>
        </>
      );
    };
    
    export default Board;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search