React井字棋游戏获胜序列高亮实现问题求助
问题描述
我正在开发React版井字棋游戏,想要实现获胜序列高亮功能。已经创建了winningSequence状态,但在checkWinner函数中调用setWinningSequence设置获胜序列时,出现了无限渲染错误。请问是否需要使用useEffect或useCallback来解决该问题?
附现有代码:
Board.jsx
import React, { useState } from "react"; import Square from "../Square/Square"; import classes from "./Board.module.css"; const Board = () =>{ const [state, setState] = useState(Array(9).fill(null)); const [isXTurn, setIsXTurn] = useState(null); const [playerSymbol, setPlayerSymbol] = useState(null); const [winningSequence, setWinningSequence] = useState([]); const checkWinner = () => { const winnerLogic = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ] for (let logic of winnerLogic){ const [a, b, c] = logic; if(state[a] != null && state[a] === state[b] && state[a] === state[c]){ // setWinningSequence([a,b,c]) // console.log(winningSequence) return state[a]; } } // setWinningSequence([]); return false; } const isWinner = checkWinner() const squareClickHandler = (index) => { if (state[index] != null || isWinner){ return; } const copyState = [...state] copyState[index] = isXTurn ? "X" : "O" setState(copyState); setIsXTurn(!isXTurn) } const handlePlayAgain = () => { setState(Array(9).fill(null)) setIsXTurn(null) setPlayerSymbol(null) setWinningSequence([]) } const handleSymbolSelection = (symbol) => { if (symbol === "X"){ setIsXTurn(true); }else{ setIsXTurn(false); } setPlayerSymbol(symbol); }; const renderSymbolSelection = () => { return ( <center> <h2>Select Your Symbol</h2> <button onClick={() => handleSymbolSelection("X")}>X</button> <button onClick={() => handleSymbolSelection("O")}>O</button> </center> ); }; const renderSquare = (index) => { // console.log(winningSequence) const isWinningSquare = winningSequence.includes(index); return ( <Square key={index} onClick={() => squareClickHandler(index)} value={state[index]} isWinningSquare={isWinningSquare} /> ); }; return ( <div className={classes["board-container"]}> {!playerSymbol ? ( renderSymbolSelection() ) : isWinner ? ( <> <center> {isWinner} Won the Game <button onClick={handlePlayAgain}>Play Again</button> </center> <div className={classes["board-row"]}> {renderSquare(0)} {renderSquare(1)} {renderSquare(2)} </div> <div className={classes["board-row"]}> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </div> <div className={classes["board-row"]}> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </div> </> ) : ( <> <center> <h4>Player {playerSymbol} please move!</h4> </center> <div className={classes["board-row"]}> {renderSquare(0)} {renderSquare(1)} {renderSquare(2)} </div> <div className={classes["board-row"]}> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </div> <div className={classes["board-row"]}> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </div> </> )} </div> ); }; export default Board;
Square.jsx
import React, { memo } from "react"; import classes from "./Square.module.css"; const Square = ({ value, onClick, isWinningSquare }) => { const squareClassName = `${classes.square} ${isWinningSquare ? classes.winning : ""}`; return ( <div className={squareClassName} onClick={onClick}> <h2>{value}</h2> </div> ); }; export default memo(Square);
我尝试创建winningSequence状态:
const [winningSequence, setWinningSequence] = useState([]);
但在checkWinner函数中尝试设置获胜序列时出现无限渲染错误:
for (let logic of winnerLogic){ const [a, b, c] = logic; if(state[a] != null && state[a] === state[b] && state[a] === state[c]){ // setWinningSequence([a,b,c]) // console.log(winningSequence) return state[a]; } }
问题原因
无限渲染的核心原因是:你在组件的渲染流程中直接调用了setWinningSequence。组件每次渲染都会执行checkWinner函数,而setWinningSequence会触发组件重新渲染,重新渲染又会再次执行checkWinner,再次调用setWinningSequence,形成无限循环。
不需要用useCallback解决这个问题,useCallback主要用于缓存函数避免不必要的重渲染,这里的问题是状态更新时机错误。
解决方案
方案一:使用useEffect监听棋盘状态变化
修改checkWinner让它同时返回获胜者和获胜序列,然后用useEffect监听state的变化,在状态更新后检查获胜并设置序列:
- 修改
checkWinner函数:
const checkWinner = () => { const winnerLogic = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let logic of winnerLogic) { const [a, b, c] = logic; if (state[a] && state[a] === state[b] && state[a] === state[c]) { return { winner: state[a], sequence: [a, b, c] }; } } return { winner: false, sequence: [] }; };
- 添加
useEffect监听state:
import React, { useState, useEffect } from "react"; // 引入useEffect // ...其他代码 useEffect(() => { const { sequence } = checkWinner(); setWinningSequence(sequence); }, [state]); // 仅当棋盘状态变化时执行 const { winner: isWinner } = checkWinner(); // 获取当前获胜状态
方案二:在落子后同步检查获胜序列
修改checkWinner让它接受棋盘状态作为参数,在squareClickHandler更新棋盘状态后,直接用新状态检查获胜并设置序列(避免React状态更新的异步问题):
- 修改
checkWinner函数:
const checkWinner = (boardState) => { const winnerLogic = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let logic of winnerLogic) { const [a, b, c] = logic; if (boardState[a] && boardState[a] === boardState[b] && boardState[a] === boardState[c]) { return { winner: boardState[a], sequence: [a, b, c] }; } } return { winner: false, sequence: [] }; };
- 修改
squareClickHandler:
const squareClickHandler = (index) => { if (state[index] != null || isWinner){ return; } const copyState = [...state]; copyState[index] = isXTurn ? "X" : "O"; setState(copyState); setIsXTurn(!isXTurn); // 用新的棋盘状态检查获胜 const { winner, sequence } = checkWinner(copyState); if (winner) { setWinningSequence(sequence); } }; const { winner: isWinner } = checkWinner(state); // 获取当前获胜状态
总结
两种方案都能解决无限渲染问题,核心是避免在组件渲染阶段直接调用状态更新函数,而是将状态更新放在副作用(useEffect)或者用户交互的回调中执行。
内容的提问来源于stack exchange,提问作者Aman Shrivastava

