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> ) }
问题根源
- React状态更新异步特性:调用
setHistory(hist)后立刻访问history拿到的仍是旧状态,且连续两次setHistory会被React批量合并,最终只执行最后一次更新,导致历史截断操作失效。 - 初始状态错误:
Array(Array(9).fill(null))会创建一个包含同一个数组引用的数组,可能导致后续棋盘状态意外共享。 - 依赖旧状态更新:添加新步骤时用的是原始
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
相关产品推荐
相关产品推荐

