React如何在numRows、numCols初始化后有条件设置state不触发Hook报错
问题根因
你当前的问题本质是在React组件还没完成DOM挂载、.gridContainer元素还没插入页面的时候,就尝试读取它的宽高,首次渲染时document.readyState还没到complete,或者就算document ready了组件对应的DOM还没渲染完成,导致numRows、numCols都是undefined,初始化的grid自然是空的。点击两次Clear能生效是因为此时组件已经完成挂载,.gridContainer元素已经存在,可以正确读取到宽高。
你之前报的「React Hook "useState" cannot be called inside a callback」错误,是因为你错误地把useState的声明语句放到了useEffect或者回调里,React要求所有Hook必须在组件顶层调用,只把setGrid(更新函数)放到回调里是完全合法的,不会报错。
解决方案
第一步:把尺寸相关变量改为响应式state,grid初始值设为空
把原来的临时变量width、height、numRows、numCols都改成state,初始值设为0,grid的初始值直接设为空数组。
第二步:用useEffect在组件挂载完成后读取DOM尺寸
等组件挂载完成后,再获取.gridContainer元素的宽高,计算出numRows和numCols,触发重渲染。
第三步:监听numRows和numCols的变化,初始化网格
当numRows和numCols都计算完成后,再调用setGrid初始化网格,不会触发Hook规则报错。
修改后的核心代码
import { useState, useRef, useCallback, useEffect } from 'react'; import produce from 'immer'; // 提前定义格子尺寸,对应你样式里的20px,你之前代码里的gridSize未声明 const gridSize = 20; // 生命游戏邻居计算规则 const operations = [ [0, 1], [0, -1], [1, -1], [-1, 1], [1, 1], [-1, -1], [1, 0], [-1, 0] ]; function App() { // 把尺寸变量改为响应式state const [numRows, setNumRows] = useState(0); const [numCols, setNumCols] = useState(0); // grid初始值设为空数组,useState放在组件顶层,符合Hook规则 const [grid, setGrid] = useState([]); const [interval, setInterval] = useState(50); const intervalRef = useRef(interval); intervalRef.current=(interval*10); const [running, setRunning] = useState(false) const runningRef = useRef(running) runningRef.current = running; const clearGrid = () => { console.log("clearGrid called"); const rows = []; for (let i = 0; i < numRows; i++) { rows.push(Array.from(Array(numCols), () => 0)) } return rows } const randomiseGrid = () => { const rows = []; for (let i = 0; i < numRows; i++) { rows.push(Array.from(Array(numCols), () => Math.random() > 0.9 ? 1 : 0)) } setGrid(rows); } const runSimulation = useCallback(() => { if (!runningRef.current || numRows === 0 || numCols === 0) { return; } setGrid((g) => { return produce(g, gridCopy => { for (let i = 0; i < numRows; i++) { for (let k = 0; k < numCols; k++) { let neighbours = 0; operations.forEach(([x,y]) => { const newI = i + x; const newK = k + y; // 边界校验 if (newI >= 0 && newI < numRows && newK >= 0 && newK < numCols) { neighbours += g[newI][newK] } }) // 应用规则 if (neighbours < 2 || neighbours > 3) { gridCopy[i][k] = 0; } else if (g[i][k] === 0 && neighbours === 3) { gridCopy[i][k] = 1; } } } }) }) setTimeout(runSimulation, intervalRef.current) },[numCols, numRows]) // 组件挂载后读取网格容器尺寸 useEffect(() => { const getGridSize = document.querySelector('.gridcontainer'); if (!getGridSize) return; const width = getGridSize.offsetWidth const height = getGridSize.offsetHeight const useableCols = Math.round(width / gridSize); const useableRows = Math.round(height / gridSize); setNumRows(useableRows); setNumCols(useableCols); }, []) // 当行数和列数计算完成后,初始化网格 useEffect(() => { if (numRows > 0 && numCols > 0) { setGrid(clearGrid()); } }, [numRows, numCols]) return ( <> <div className="flex flex-col w-screen h-screen bg-white"> <div className="flex items-center justify-end h-16 bg-white px-11"> <div className="flex items-center justify-center text-sm font-bold tracking-tight text-right text-black uppercase w-72">george conway's game of life</div> </div> <main className="flex flex-row w-full h-full p-11"> <div className="w-full h-full bg-gray-100 rounded gridcontainer" style={{display: "grid",gridTemplateColumns: `repeat(${numCols}, ${gridSize}px)`}} > {grid.map((rows, i) => rows.map((col, k) => <div key={`${i}-${k}`} onClick={() => { const newGrid = produce(grid, gridCopy => { gridCopy[i][k] = grid[i][k] ? 0 : 1; }) setGrid(newGrid) }} className={`w-5 h-5 border border-gray-300 ${grid[i][k] ? 'bg-black' : undefined}`}/> ))} </div> <div className="flex flex-col items-center h-full space-y-12 w-72 ml-11"> <div> <h3 className="text-sm text-center text-gray-700 uppercase">rules (how things evolve)</h3> <ul className="flex flex-col mt-4 space-y-4"> <li className="text-sm font-light text-center text-gray-600">Any live cell with fewer than two or more than three live neighbours dies</li> <li className="text-sm font-light text-center text-gray-600">Any dead cell with exactly three live neighbours becomes a live cell</li> </ul> </div> <button onClick={() => {setRunning(!running) if (!running) { runningRef.current = true; runSimulation(); } }} className="flex items-center justify-center w-48 h-12 font-medium text-white bg-gray-500 rounded focus:outline-none"> {running ? 'stop' : 'evolve'} </button> <button onClick={randomiseGrid} className="flex items-center justify-center w-48 h-12 font-medium text-white bg-gray-500 rounded focus:outline-none"> randomise </button> <button onClick={() => {setGrid(clearGrid())}} className="flex items-center justify-center w-48 h-12 font-medium text-white bg-gray-500 rounded focus:outline-none"> clear </button> <div className="w-72"> <div className="mb-10 text-sm font-light text-center text-gray-600 uppercase">set interval</div> <Slider min={1} max={100} step={1} color="gray" defaultValue={50} labelAlwaysOn value={interval} onChange={setInterval} /> </div> <p className="text-sm font-light text-center text-gray-600"> The Game of Life is a cellular automaton devised by Dr John Conway in 1970. The game is a zero-player game, meaning that its evolution is determined by its initial state. One interacts with the Game of Life by creating an initial configuration and observing how it evolves. </p> </div> </main> </div> </> ); }
额外优化建议
如果需要支持窗口缩放时自动调整网格尺寸,可以给.gridContainer元素加ResizeObserver监听尺寸变化,实时更新numRows和numCols即可。
内容的提问来源于stack exchange,提问作者Ray Purchase

