1

I'm trying to make a tic-toc game without using classes and with player-computer. The whole code in codesandbox.io

The piece of code, where the problems appear:

const handleClick = (i) => {
  let squares2 = squares.slice();
  if (calculateWinner(squares) || squares[i]) {
    return;
  }

  new Promise((resolve) => {
    setSquares((squares) => squares.splice(i, 1, "X"));
    setXIsNext(!xIsNext);
    resolve(squares);
  })
  .then((squares) => {
    if (!squares.includes(null)) {
      return squares;
    }
    setTimeout(() => {
      squares2 = computersTurnHandler(squares);
      setSquares((squares) => [...squares2]); // here appears the second problem
    }, 500);
  })
  .then(() => {
    setXIsNext(!xIsNext);
  });

    if (!squares.includes(null)) {
    setIsFinished(true);
    }
};

The first problem is, that setTimeout flashes elements from array in the UI until it executes. I tried to move setSquares(squares => [...squares2]); away from setTimeout, but it caused me another problems, f.e. text stopped to appear on the screen, array stopped to update.

setTimeout(() => {
  squares2 = computersTurnHandler(squares);
  setSquares(squares => [...squares2]);
}, 2000)

The second problem is, that sometimes while playing it throws me an error and I can't understand, why this is happening. In browser:

Uncaught TypeError: squares2 is undefined handleClick Board.js:34 // (line 40 in codesandbox)

In codesandbox:

TypeError Invalid attempt to spread non-iterable instance. In order to be iterable, non-array objects must have a Symbol.iterator method.

Finally, is it acceptable at all to use Promises in that case (and is that code at all not so unacceptable : D)? I found there that useState is asynchronous, and using Promise solved the problem of the state not updating in time.

Will appreciate any help!

Mrmld Sky
  • 139
  • 1
  • 7

1 Answers1

3

There are few things that need to be fixed.

  1. In your handleClick, you are using setSquares((squares) => squares.splice(i, 1, "X")). squares.splice will mutate the state variable squares which one should never do.
  2. Also splice doesn't return updated array but

An array containing the deleted elements.

This causes squares from [null, null, null, null, null, null, null, null, null] to [null] causing you board to become blank for a moment. Then your setSquares((squares) => [...squares2]) kicks in from setTimeout making squares back to array making your square values visible again hence, the flash.

  1. computersTurnHandler might return undefined in some cases where none of the return statements trigger hence

Uncaught TypeError: squares2 is undefined handleClick Board.js:34

  1. You need to prevent user from clicking until your setTimeout is complete else multiple rapid clicks by user will cause inconsistent behaviour.

As for your query

is it acceptable at all to use Promises in that case

Usually situations like these can simply be solved by making use of temporary variable in most cases.

Now the Solution with promise and fixed handleClick and computersTurnHandler is as follows

// add this state variable
const [allowClick, setAllowClick] = useState(true);

const handleClick = (i) => {
    if (!allowClick || calculateWinner(squares) || squares[i]) {
      return;
    }
    
    // use this temporary variable to keep things in sync and get around the 
    // asyn nature of 'setSquare'
    let squares2 = squares.slice();

    // disable user move until setTimeout executes
    setAllowClick(false);

    new Promise((resolve) => {
      squares2[i] = "X";
      setSquares(squares2);
      setXIsNext(!xIsNext);
      // since we are using temporary variable 'squares2',
      //  we don't care much about value to be passed in resolve
      // we could simply do 'resolve()'
      resolve(squares2);
    })
    // if 'resolve()' is used then we use 
    // '.then(() => {....})'
      .then((sqr) => {
        if (!squares2.includes(null)) {
          return squares2;
        }
        setTimeout(() => {
          squares2 = computersTurnHandler(squares2);
          setSquares(squares2);

          // now user can make next move
          setAllowClick(true)
        }, 500);
      })
      .then(() => {
        setXIsNext(!xIsNext);
      });

    if (!squares.includes(null)) {
      setIsFinished(true);
      console.log(isFinished);
    }
  };
const computersTurnHandler = (sq1) => {
    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]
    ];
    const sq = [...sq1];
    for (let i = 0; i < lines.length; i++) {
      const [a, b, c] = lines[i];
      if (sq[a] === "O" && sq[b] === "O" && sq[c] === null) {
        sq[c] = "O";
        return sq;
      }
      if (sq[a] === "O" && sq[c] === "O" && sq[b] && null) {
        sq[b] = "O";
        return sq;
      }
      if (sq[b] === "O" && sq[c] === "O" && sq[a] === null) {
        sq[a] = "O";
        return sq;
      }
      if (sq[a] === "X" && squares[b] === "X" && sq[c] === null) {
        sq[c] = "O";
        return sq;
      }
      if (sq[a] === "X" && sq[c] === "X" && sq[b] && null) {
        sq[b] = "O";
        return sq;
      }
      if (sq[b] === "X" && sq[c] === "X" && sq[a] === null) {
        sq[a] = "O";
        return sq;
      }
    }
    let random;
    while (!sq[random]) {
      random = Math.floor(Math.random() * sq.length);
      if (!sq[random]) {
        sq[random] = "O";
        return sq;
      }
    }
    // if no other retuns are triggered then return sq
    return sq;
  };

Keeping fixes in computersTurnHandler below is handleClick without promise

const handleClick = (i) => {
    if (isFinished || squares[i]) {
      return;
    }

    let squares2 = squares.slice();

    squares2[i] = "X";
    if (calculateWinner(squares2) || !squares2.includes(null)) {
      setSquares(squares2);
      setIsFinished(true);
      return;
    }

    squares2 = computersTurnHandler(squares2);

    if (calculateWinner(squares2) || !squares2.includes(null)) {
      setSquares(squares2);
      setIsFinished(true);
      return;
    }

    setSquares(squares2);
  };

Note that in case of solution without promise turn will always be X since move by computer will be synchronous in nature. Also computersTurnHandler might need some more fixing since in some cases computer stops making move.

Rishabh Singh
  • 353
  • 2
  • 8