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]) }