I'm building my first react app and came across this situation in which I have to call this.setState() twice within a method. One of those calls is made inside a nested function called from the method as well.
In my case, I'm building a minesweeper game, and what I'm trying to achieve is to find out if a player has actually won after flagging all mines correctly. A player wins when setting flags over every cell that contains a mine so you cannot click on it. To do so I've created a method called checkIfWin() that does the following:
- store in an array the board data information (every cell with its properties) and in a counter the number of remaining mines. Both are extracted from the component state in two variables called data and minesLeft
- If there are 0 mines remaining (user has flagged as many cells as there are mines in the game), you then compare two arrays (minesArray containing every mine position in the board and flagsArray containing every flag position)
- If the arrays contain the same info (flags are covering every mine), you win. If the arrays differ, keep playing.
This function is called from two different places
- After a player clicks the last cell that doesn't contain a mine and every tile containing flag has been properly flagged
- After a player flags a cell and the remaining mines to cover in the board is 0 In the first situation there is no problem it works fine. I check if minesLeft in status is === 0 and if so I call the method checkIfWin(). Since minesLeft only decreases after flagging a cell, not by clicking one, there's no problem here.
But after a player flags on a cell, I cannot manage to find a solution to declare if a player has won the game or not. This is my flag() logic:
- Check if a tile it is covered (it hasn't been clicked on) and if the player didn't win yet (to prevent clicking tiles after the game has ended)
- If a tile is covered and flagged, unflag it and update counter.
- If tile is covered, and it is not flagged then cover it with a flag and decrease the minesLeft counter.
- When minesLeft is 1 and Player flags a new cell, minesLleft is now 0, so this is when I have to check for a win.
- Now this is where i would like to check if the player did win To do so, I would have to call this.setState() to update the flagsArray with the position of the new flag on the board and later call checkIfWin() -that extracts a value stored inside state- from within this method as well.
This results in the this.setState() call to batch up before executing fully so that when I try to compare if minesLeft===0 inside checkIfWin() the value stored in state is the value state had before the last flagging (1 instead of 0), it didn't update because of React's stacking up logic for this.setState() calls. Also mentioned on JusticeMba article on medium
React may batch multiple setState() calls into a single update for performance.
So WHAT I WANT TO KNOW (sorry for the length of the question) is if there is any way that I could do this? I'm actually binding this flagging method as a right click handler on a component inside render() of the Board. Code will follow below. I don't care about if you know some solution in code or pseudo code. I would take both it in, since I don't seem to find a proper fix for this.
Code:
Inside Board.js render()
render() {
const board = this.renderBoard(this.state.boardData);
return (
<div className={classes.board}>
<div className={classes.gameInfo}>
<h1>
{this.state.gameStatus}
</h1>
<span className={classes.info}>
Mines remaining: {this.state.mineCount}
</span>
</div>
{board} //This is a Board component that holds Tiles to which the rightClickHandler() is binded as a prop
</div>
The rightClickHandler() that manages flagging of Tiles:
rightClickHandler(event, x, y) {
event.preventDefault(); // prevents default behaviour such as right click
const clickedTile = { ...this.state.boardData[x][y] }
//ommit if revealed and if game is ended
if (!clickedTile.isRevealed && !(this.state.gameStatus === "YOU WON!")) {
let minesLeft = this.state.mineCount;
//if not revealed it can be flagged or not
if (clickedTile.isFlagged) {
//if flagged, unflag it
clickedTile.isFlagged = false;
minesLeft++;
} else {
//if not flagged, flag it
clickedTile.isFlagged = true;
minesLeft--;
}
//Update the state with new tile and check game status
const updatedData = [...this.state.boardData];
updatedData[x][y] = { ...clickedTile };
this.setState({
boardData: updatedData,
mineCount: minesLeft,
});
//THIS IS WHERE I WOULD LIKE TO checkIfWin()
}
Lastly, checkIfWin():
//Check game progress and returns a boolean: true if win, false if not.
checkIfWin() {
//No need to create a new array, I'll just reference the one inside state
const data = this.state.boardData;
const minesLeft = this.state.mineCount;
//If flagged as many tiles as mines were initially
if (minesLeft === 0) {
//get mines as an array of positions
const mineArray = this.getMines(data);
//get flags as array of positions
const flagArray = this.getFlags(data);
if (JSON.stringify(mineArray) === JSON.stringify(flagArray)) {
//if both arrays are equal, game is won
return true;
}
} else {
//if more than 0 mines are left return false
return false;
}
}
TL;DR: I'm trying to access the value of a component's property with an outdated state that was supposed to be updated from a previous this.setState() call inside a click handler of a JSX component inside render()
Sorry for the length, and I hope it is understandable, if not I can edit for clarity.