You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.10 08:06:02