Stop an update when a condition is met (React tic-tac-toe)

Viewed 176

I'm creating a quick tic-tac-toe implementation in React, and ran into an issue, seen here: enter image description here

Here is the code:

https://codesandbox.io/s/cold-grass-yr6mn?file=/src/container/Board.jsx

The Problems:

1.) It prints the wrong winner (the game changes the turn after the user wins)

2.) When a user has won, I want to display "Winner: X", instead of "Next: X".

Why it's failing:

In my Board.jsx, I pass the following method down to my Square.jsx component, which updates the board state, and the player turn:

  const updateBoard = (squareIndex) => {
    setGameState((prevBoard) => {
      // handle previously set value
      if (prevBoard[squareIndex] !== "") {
        return prevBoard;
      }
      // otherwise update the board
      let updatedBoard = [...prevBoard];
      updatedBoard[squareIndex] = playerTurn;
      return updatedBoard;
    });

    // update player turn
    setPlayerTurn((turn) => (turn === "X" ? "O" : "X"));
  };

In the same file I have a useEffect that checks when the gameState or player turn update. This method determines the winner.

  // board update events/actions
  useEffect(() => {
    const isWinner = () => {
      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 (
          gameState[a] &&
          gameState[a] === gameState[b] &&
          gameState[a] === gameState[c]
        ) {
          return true;
        }
      }
      return false;
    };

    if (isWinner()) {
      alert(`${playerTurn} is the winner.`);
      return;
    }
  }, [gameState, playerTurn]);

So the reason why it is printing the wrong winner is because once someone wins, it will still update the player turn, and then will display the losing player.

My Question

What is the best way to handle updating player turn? If I set the player in my useEffect, I will get an infinite render loop. I can think of some hacky ways to fix it like printing the opposite of whatever the current player turn is as the winner. But that does not seem ideal.

Any advice on the best way to approach this fix is appreciated.

Thanks!

3 Answers

I have tried to simplify the logic a little bit.

Link: https://codesandbox.io/s/elastic-moon-bcps0?file=/src/container/Board.jsx:0-1829

JS:

import React, { useEffect, useState } from "react";
import Square from "../components/Square/Square";

import styles from "./Board.module.css";

const Board = () => {
  const [gameState, setGameState] = useState([]);
  const [playerTurn, setPlayerTurn] = useState("X");

  // onMount / todo: when user resets board
  useEffect(() => {
    const initializeGame = () => setGameState(new Array(9).fill(""));
    initializeGame();
  }, []);

  // board update events/actions
  useEffect(() => {
    let winner = "";
    const isWinner = () => {
      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 (
          gameState[a] &&
          gameState[a] === gameState[b] &&
          gameState[a] === gameState[c]
        ) {
          winner = gameState[a];
          return true;
        }
      }
      return false;
    };

    if (isWinner()) {
      alert(`${winner} is the winner.`);
    }
  }, [gameState]);

  const updateBoard = (squareIndex) => {
    if (gameState[squareIndex] !== "") {
      return;
    }

    let updatedBoard = [...gameState];
    updatedBoard[squareIndex] = playerTurn;

    setGameState(updatedBoard);
    // update player turn
    setPlayerTurn((turn) => (turn === "X" ? "O" : "X"));
  };

  return (
    <>
      <main className={styles.board}>
        {gameState.map((cell, position) => (
          <Square
            boardPosition={position}
            displayValue={cell}
            updateBoard={updateBoard}
            key={position}
          />
        ))}
      </main>
      <div>
        <p>Next: {playerTurn}</p>
      </div>
    </>
  );
};

export default Board;

You can try this:

  • Add a state variable to maintain state. Initialize it as false
const [gameFinished, setFinished] = useState(false);
  • Add a prop to Square as disabled.
    1. Call callback on click based on this:
const clickHandler = () => {
  if (!isDisabled) updateBoard(boardPosition);
};
  1. Update className based on this
if (isDisabled) {
  classes.push("disabled");
}
  • Use newly create state variable to pass prop
isDisabled={gameFinished}

Sandbox link

  1. You could update the player turn in your useEffect keeping the gameState as the dependency.
  2. If the isWinner function returns true, then use another type of value to setPlayerTurn to declare winner rather than using another state.
const WINNING_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]
];

function isWinner(gameState) {
  for (let i = 0; i < WINNING_LINES.length; i++) {
    const [a, b, c] = WINNING_LINES[i];
    if (
      gameState[a] &&
      gameState[a] === gameState[b] &&
      gameState[a] === gameState[c]
    ) {
      return true;
    }
  }
  return false;
};

const Board = () => {
  const [gameState, setGameState] = useState(new Array(9).fill(""));
  const [playerTurn, setPlayerTurn] = useState("X");


  const updateBoard = (squareIndex) => {
    setGameState((prevBoard) => {
      // handle previously set value
      if (prevBoard[squareIndex] !== "") {
        return prevBoard;
      }
      // otherwise update the board
      let updatedBoard = [...prevBoard];
      updatedBoard[squareIndex] = playerTurn;
      return updatedBoard;
    });
  };

   React.useEffect(() => {
    if (isWinner(gameState)) {
      setPlayerTurn(null);
    } else {
      setPlayerTurn((turn) => (turn === "X" ? "O" : "X"));
    }
  }, [gameState]);

  return (
    <>
      <main className={styles.board}>
        {gameState.map((cell, position) => (
          <Square
            boardPosition={position}
            displayValue={cell}
            updateBoard={updateBoard}
            key={position}
          />
        ))}
      </main>
      <div>
        <p>Next: {playerTurn}</p>
      </div>
    </>
  );
};

export default Board;

Related