How do I access a component's state in a function that is inside another function that calls this.setState()?

问题: 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 fun...

问题:

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:

  1. 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
  2. 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)
  3. 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

  1. After a player clicks the last cell that doesn't contain a mine and every tile containing flag has been properly flagged
  2. 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, theres 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:

  1. Check if a tile it is covered (it hasn't been clicked on) and if the player didnt win yet (to prevent clicking tiles after the game has ended)
  2. If a tile is covered and flagged, unflag it and update counter.
  3. If tile is covered, and it is not flagged then cover it with a flag and decrease the minesLeft counter.
  4. 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.
  5. 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 didnt update because of Reac't'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? Im actually binding this flagging method as a right click handler on a component inside render() of the Board. Code will follow below. I dont 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, Ill 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: Im trying to acces the value of a component's property with an outdated state that was suppossed to be updated from a previos 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.


回答1:

Assuming I understood it right, you could simply pass checkIfWin as a callback to your setState.

rightClickHandler(event, x, y) {
    // Rest of your code...

    // Pass a callback to your setState
    this.setState({
        boardData: updatedData,
        mineCount: minesLeft,
    }, this.checkIfWin);

}

回答2:

this.setState() has a callback, so up there you are just providing the what to run.

this.setState(state, callback);

So, up there what's happening there is:

this.setState({
    boardData: updatedData,
    mineCount: minesLeft,
},
() => {
    // Check if the player won or not
});

Also you can write it alternatively:

checkIfWin(){
    // Check if the player won or not
}

updateState(){

    this.setState(
        {
            boardData: updatedData,
            mineCount: minesLeft,
        }, 
        this.checkIfWin());
}

Hope it was understandable.


回答3:

Following Felipe Lanza and Nomi G approach and by understanding how react setState callback works, I've come to a working solution that is as follows:

  1. After a user flags as many tiles as there are mines in the board (minesLeft === 0), I use setState callback with checkIfWin() to check if the player had won
  2. I've modified checkIfWin() to update the state in case the game is won, instead of returning a boolean.

This is what my code looks like: Inside rightClickHandler():

rightClickHandler(event, x, y) {
   // code...

   // Pass a callback to your setState
   if(minesLeft === 0) {

        //If user flagged possible last tile containing a mine, check if won with a setState callback to checkIfWin()
        this.setState(
         {
            boardData: updatedData,
            mineCount: minesLeft,
         }
     ,() => {
                this.checkIfWin(this.state.mineCount);
            });
 }  else {
    //Rest of logic
}

And this is what my tweaked checkIfWin() looks like now:

checkIfWin() {
    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. Update gameStatus
            this.setState({
                gameStatus: "YOU WON!"
            });
        }
    } 
}
  • 发表于 2018-09-01 04:15
  • 阅读 ( 249 )
  • 分类:sof

条评论

请先 登录 后评论
不写代码的码农
小编

篇文章

作家榜 »

  1. 小编 文章
返回顶部
部分文章转自于网络,若有侵权请联系我们删除