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

React Hooks setState数组问题:井字棋回溯功能失效排查

React函数式组件井字棋回溯功能异常修复

我用函数式组件改写React井字棋教程代码后,其他功能正常,但回溯功能出现异常。排查发现handleClick函数中setHistory(hist)并未实际修改history状态(hist是数组的数组)。以下是Game组件原代码:

const Game = () => { 
    
    const [xIsNext, setXIsNext] = useState(true);
    const [history, setHistory] = useState(Array(Array(9).fill(null)));
    const [stepNumber, setStepNumber] = useState(0);

    function handleClick(i) {        
        const hist = history.slice(0, stepNumber+1);
        console.log(hist)
        setHistory(hist);
        console.log("in handleClick - log 1 - history size = "+ history.length)

        const currentBoard = [...history[stepNumber]];
        const winner = calculateWinner(currentBoard);
        
        if (winner || currentBoard[i]) return;
        currentBoard[i] = xIsNext ? "X" : "O";
        setHistory(history.concat([currentBoard]));
        setStepNumber (stepNumber + 1)
        setXIsNext(!xIsNext);

    }

    function jumpTo(move) {
        setXIsNext(move % 2 == 0)
        setStepNumber(move)
    }
    

    const currentBoard = [...history[stepNumber]];
    const winner = calculateWinner(currentBoard);

    const moves = history.map((step, move) => {
        const description = move ?
            ' Go to move #' + move :
            ' Go to game start';
        return (
            <li key={move}>
                <button onClick={() => jumpTo(move)} > {description} </button>
            </li>
        );
    })

    let status;
    if (winner) {
        status = winner + " WON !!!!";
    }
    else {
        status = 'Next player: ' + (xIsNext ? "X" : "O");
    }

    return (
        <div className="game">
            <div className="game-board">
                <Board 
                    squares = {currentBoard} 
                    handleClick={handleClick}
                />
            </div>
            <div className="game-info">
                <div className="status">{status}</div>
                <ol>{moves}</ol>
            </div>
        </div>
    )
}

问题根源

  1. React状态更新异步特性:调用setHistory(hist)后立刻访问history拿到的仍是旧状态,且连续两次setHistory会被React批量合并,最终只执行最后一次更新,导致历史截断操作失效。
  2. 初始状态错误:Array(Array(9).fill(null))会创建一个包含同一个数组引用的数组,可能导致后续棋盘状态意外共享。
  3. 依赖旧状态更新:添加新步骤时用的是原始history而非截断后的hist,导致回溯后添加新步骤仍基于完整历史,无法覆盖后续步骤。

修复后的代码

const Game = () => { 
    
    const [xIsNext, setXIsNext] = useState(true);
    // 修复初始状态:确保初始历史是包含独立空棋盘的数组
    const [history, setHistory] = useState([Array(9).fill(null)]);
    const [stepNumber, setStepNumber] = useState(0);

    function handleClick(i) {        
        // 先截断历史到当前步数
        const truncatedHistory = history.slice(0, stepNumber + 1);
        // 基于截断后的历史获取当前棋盘
        const currentBoard = [...truncatedHistory[truncatedHistory.length - 1]];
        const winner = calculateWinner(currentBoard);
        
        if (winner || currentBoard[i]) return;
        
        currentBoard[i] = xIsNext ? "X" : "O";
        // 基于截断后的历史添加新步骤,一次完成状态更新
        setHistory([...truncatedHistory, currentBoard]);
        // 新步数为截断后的历史长度(添加了一步)
        setStepNumber(truncatedHistory.length);
        setXIsNext(!xIsNext);
    }

    function jumpTo(move) {
        setXIsNext(move % 2 === 0);
        setStepNumber(move);
    }
    

    const currentBoard = [...history[stepNumber]];
    const winner = calculateWinner(currentBoard);

    const moves = history.map((step, move) => {
        const description = move ?
            ' Go to move #' + move :
            ' Go to game start';
        return (
            <li key={move}>
                <button onClick={() => jumpTo(move)} > {description} </button>
            </li>
        );
    })

    let status;
    if (winner) {
        status = `${winner} WON !!!!`;
    }
    else {
        status = `Next player: ${xIsNext ? "X" : "O"}`;
    }

    return (
        <div className="game">
            <div className="game-board">
                <Board 
                    squares={currentBoard} 
                    handleClick={handleClick}
                />
            </div>
            <div className="game-info">
                <div className="status">{status}</div>
                <ol>{moves}</ol>
            </div>
        </div>
    )
}

关键修复点

  • 修正初始状态为[Array(9).fill(null)],确保每个棋盘状态都是独立数组。
  • 移除多余的setHistory(hist)调用,直接基于截断后的历史构建新状态,避免异步更新导致的状态不一致。
  • 从截断后的历史中获取当前棋盘,确保回溯后点击时使用正确的棋盘状态。
  • 用truncatedHistory.length更新stepNumber,保证步数与历史长度匹配。

内容的提问来源于stack exchange,提问作者ratchek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 20:36:17