Skip to content
Advertisement

ReactJS OnClick Function that Changes State Not Rerendering

I am trying to create a floodfill visualization that allows the user to enter the grid size and the grid adjusts dynamically however, the grid is composed of buttons that have an onClick function that would then trigger the floodfill. I read up on the setState function and it says its asynchronous which is the reason that it’s not rerendering and that you should use a callback, however I’m not sure how to do that. When I put the recursive functions into the callback, an error occurs.

const Page =() => {

  const [matrix, setMatrix] = useState(Array.from({length: 3},()=> Array.from({length: 3}, () => 0)));
  const [size, setSize] = useState('');
  const [display, setDisplay] = useState('');
  const COLORS = ['red', 'blue', 'yellow'];

  const floodFill = (grid, i, j, currColor) => {
    console.log(grid, matrix, i, j, currColor)
    let newColor = 0;

    //Checks in range
    if(i<0 || i>size-1 || j< 0 || j>size-1){
      console.log("out of bounds", i, j , size);
      return;
    }

    //Determines the other color
    if(currColor === 1){
      newColor = 2;
    }
    else{
      newColor = 1;
    }

    if(grid[i][j] === newColor){
      console.log("already the color" + newColor);
      return;
    }

    grid[i][j]= newColor;
    setMatrix(grid);
    
    console.log("new color: " + grid[i][j])

    floodFill(grid, i+1, j, currColor);
    floodFill(grid, i-1, j, currColor);
    floodFill(grid, i, j+1, currColor);
    floodFill(grid, i, j-1, currColor);


  }

  //handles the input 
  const handleChange = ({target}) => {
    const newSize = target.value
    console.log(newSize);

    //If theres nothing in the box then default to 1
    if(!newSize){
      setDisplay('');
      setSize(3);
      return;
    }

    //Makes sure the input is within the range
    if(newSize === '1'){
      setSize(3)
      setDisplay(1)
    }
    else if(newSize === '2'){
      setSize(3)
      setDisplay(2)
    }
    else if(newSize <= 100 && newSize > 2 && newSize !== size){
      setSize(newSize);
      setDisplay(newSize);

      //update the matrix (default set to random values)=
      setMatrix(Array.from({length: newSize},()=> Array.from({length: newSize}, () => Math.floor((Math.random() * 2) + 1))))
    }
  }

  let grid = matrix;

  return(
    
    <div className = "Page">

      <div className = "head">

        <h1>Grid size? (3-100): </h1>
        <input type = "number" value={display} onChange={handleChange} id='size-input' />
        <h1>Size of graph: {size}</h1>
        {matrix}
      </div>

      <div className="grid">
        {matrix.map((row, i)=> {
          return (
            <div className = "board-row" key = {i}>
              { row.map((col, j) => 
                <button 
                    key = {i + " "+ j}
                    onClick={() => floodFill(grid, i, j, matrix[i][j])} 
                    className="square" 
                    style={{backgroundColor: COLORS[matrix[i][j]]}}>
                  {matrix[i][j]}
                </button>)}
            </div>
          )})
        }
      </div>
      
    </div>
  )
}

How I tried to use the callback function:

grid[i][j]= newColor;
setMatrix(grid, () => {
  floodFill(grid, i+1, j, currColor);
  floodFill(grid, i-1, j, currColor);
  floodFill(grid, i, j+1, currColor);
  floodFill(grid, i, j-1, currColor);
});

I also tried this method which worked for small grids but created a maximum stack error for anything larger than a 3×3 grid.

setMatrix(oldMatrix => {
  const matrixCopy = matrix.map(([...i]) => i); // clone oldMatrix
  matrixCopy[i][j] = newColor; // update your copied array
  return matrixCopy; // return the new state.
});

Advertisement

Answer

React detects changes using reference comparison, so you must modify your array in immutable way.

The easiest is to

  • remove setMatrix from the floodFill function
  • add a second Fill function which calls your current fill function.
  • set the Matrix in the new function, creating a new matrix.
  • update the button.onClick handler to use the new function
  const applyFloodFill = (grid, i, j, currColor) => {
    floodFill(grid, i, j, currColor);
    setMatrix([...grid])
  }
User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement