Why isn’t my state updating correctly!

If you’ve built anything with React then you’re probably familiar with setState(). The function is called on a component to update data stored in the state of that component. The way that state is managed in React is one of the coolest reasons to use React in the first place. When the state is changed your UI is automatically re-renders and your changes propagate through your application without you having to do anything.

setState() Is Not A Synchronous Operation

What can be confusing for a newbie like me, is that setState() does not synchronously update the state. It can, but it depends on what triggered setState() to be called in the first place. This came up in my tic-tac-toe application when adding AI and I wanted to write a quick blog post because I can see this coming up again and again for new React developers.

A Specific Example

In my application, after a player clicks on the tic-tac-toe board to place a move I need to do two things. First, I need to see if that move is valid and then, if it is a valid move, I need to check to see if the game ended as a result. If not I need to allow the AI player to take their move and again check to see if the game is over before returning control to the player.

I wanted to accomplish those goals basically just how I described. I evaluated the player move, then updated the state of the game board and then attempted to pass that new state into my AI function. Unfortunately, under certain circumstances calls to setState() are batched for performance gains (see the react docs) and the second time I attempted to use the state I was actually accessing the original state prior to my changes. The actual call to setState() was not completed until after executing the remainder of my code. Meaning my AI was evaluating a board state that was no longer valid.

Once you understand that your calls to setState() are not synchronous, it’s a pretty simple fix. Just work from a copy of the state and only update once just before returning control back to the player. The code below demonstrates my solution.

gameLoop(move){
  //Get the current state of the game
  let player = this.state.turn;
  let currentGameBoard = this.validMove(move, player, this.state.gameBoard);
  if(this.winner(currentGameBoard, player)){
    this.setState({
      gameBoard: currentGameBoard,
      winner: player
    });
    return;
  }
  if(this.tie(currentGameBoard)) {
    this.setState({
      gameBoard: currentGameBoard,
      winner: 'd'
    });
    return;
  }
  player = 'o';
  currentGameBoard = this.validMove(this.findAiMove(currentGameBoard), player, currentGameBoard);
  if(this.winner(currentGameBoard, player)){
    this.setState({
      gameBoard: currentGameBoard,
      winner: player
    });
    return;
  }
  if(this.tie(currentGameBoard)) {
    this.setState({
      gameBoard: currentGameBoard,
      winner: 'd'
    });
    return;
  }
  this.setState({
    gameBoard: currentGameBoard
  });
}

As you can see, rather than attempt to keep my state up to date in the component, I’m working from a copy and only calling the setState() function to update my state when returning from my gameLoop().

If you’re looking for additional resources on setState() and it’s behavior based on the method in which it’s called, check out this blog post by Ben Nadel, it was the main reason I figured out what was going on in the first place.