首页 > 解决方案 > 如何在 React 中有条件地设置状态?

问题描述

我有一个模拟 John Conway 的生命游戏的 React SPA。在初始化/加载时,应用程序需要检查 .gridContainer 元素的高度和宽度,以便它知道要为网格绘制多少行和列(参见参考文献 1)。问题是,网格没有在加载时初始化。但是,如果您单击“清除”按钮两次,由于某些奇怪的原因会初始化。请查看 game-of-life-sage.vercel.app 并单击“清除”两次。- 包括控制台日志。

从控制台记录所有内容,似乎只有在定义 numRows 和 numCols 之后调用 clearGrid 函数时,网格才会初始化,这是有道理的。仅在初始化 numRows 和 numCols 而不创建“有条件地调用 React Hook “useState”错误时,我如何设置网格(下面的参考 2 )?我不能把它放在一个依赖 numRows 和 numCols 的 useEffect 中,因为我得到“不能在回调中调用 React Hook “useState””。

function App() {

let width;
let height;
let numRows;
let numCols;

// REF 1
if (document.readyState === 'complete') {
    let getGridSize = document.querySelector('.gridcontainer');
    width = getGridSize.offsetWidth
    height = getGridSize.offsetHeight

    let useableCols = (width / gridSize);
    let useableRows = (height / gridSize);
    numRows = Math.round(useableRows);
    numCols = Math.round(useableCols);
}

if (document.readyState === 'complete') {
    console.log("numRows: ",numRows);
    console.log("numCols: ",numCols);
}

const clearGrid = () => {
    console.log("clearGrid called");
    const rows = [];
    for (let i = 0; i < numRows; i++) {
        rows.push(Array.from(Array(numCols), () => 0))
    }
    return rows
}

// REF 2
const [grid, setGrid] = useState(() => {
    console.log("init grid state");
    return clearGrid()
})

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 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) {
        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;

                        // check if out of bounds
                        if (newI >= 0 && newI < numRows && newK >= 0 && newK < numCols) {
                            neighbours += g[newI][newK]
                        }
                    })

                    // apply rules
                    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(() => {
    console.log("grid: ",grid)
},[])

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}, 20px)`}}
            >
                {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>
    </>
);
}

非常感谢任何帮助。

编辑:

如果我在 onMount useEffect 中初始化网格状态,会发生以下情况:

使用效果错误

标签: javascriptreactjs

解决方案


正如 Drew 所说,您不能在 useEffect 中使用钩子(必须在每次渲染时调用所有钩子)

您的代码中的问题是您在第一次渲染时grid使用初始化。clearGrid()numRows在该函数中使用仍未定义。

你可以使用useRef和 useEffect 来初始化你的网格

const [grid, setGrid] = useState();
const gridcontainer = useRef(null);

width = gridcontainer.current?.offsetWidth // consider that might be undefined
height = gridcontainer.current?.offsetHeight
let useableCols = (width / gridSize);
let useableRows = (height / gridSize);
numRows = Math.round(useableRows);
numCols = Math.round(useableCols);

const clearGrid = () => {/*...*/}

useEffect(()={
    if(gridcontainer.current){
        // gridcontainer is loaded
        setGrid(clearGrid())
    }
}, [gridcontainer.current])
return (
    [...]
    <div
        ref={gridcontainer} // pass the reference to useRef
        className="w-full h-full bg-gray-100 rounded gridcontainer"
        style={{display: "grid",gridTemplateColumns: `repeat(${numCols}, 20px)`}}
    >
    [...]
)


推荐阅读