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

React井字棋游戏获胜序列高亮实现问题求助

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的变化,在状态更新后检查获胜并设置序列:

  1. 修改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: [] };
};
  1. 添加useEffect监听state:
import React, { useState, useEffect } from "react"; // 引入useEffect

// ...其他代码

useEffect(() => {
    const { sequence } = checkWinner();
    setWinningSequence(sequence);
}, [state]); // 仅当棋盘状态变化时执行

const { winner: isWinner } = checkWinner(); // 获取当前获胜状态

方案二:在落子后同步检查获胜序列

修改checkWinner让它接受棋盘状态作为参数,在squareClickHandler更新棋盘状态后,直接用新状态检查获胜并设置序列(避免React状态更新的异步问题):

  1. 修改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: [] };
};
  1. 修改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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 22:54:54