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

React井字棋开发:如何避免未点击Square子组件不必要重渲染

React井字棋全方格重渲染问题修复

问题根因

React默认渲染规则是父组件一旦重渲染,所有子组件会默认跟随重渲染,不会自动判断props是否变化,你之前给handleClick加空依赖useCallback不生效,核心是三个问题没解决:

  • Square组件没有做props比较逻辑,默认每次父组件渲染就跟着重渲染
  • Board组件渲染Square时,每次都传新创建的内联箭头函数() => props.onClick(i),就算上层传的onClick没变,这个内联函数都是全新引用,缓存会直接失效
  • 空依赖的useCallback会让函数拿到初始状态的闭包,本身逻辑就会出错,更起不到缓存作用

分步修复方案

1. 给Square组件加memo缓存

React.memo会对组件传入的props做浅比较,只有props实际发生变化时才会触发组件重渲染,修改Square组件代码:

import { memo } from 'react';
const Square = memo(({ value, onClick, ...rest }) => {
  return (
    <button className="square" onClick={onClick} {...rest}>
      {value}
    </button>
  );
});
export default Square;

这里加...rest是为了后续透传方格索引的自定义属性。

2. 移除Board组件内的内联回调,避免生成新函数引用

之前renderSquare里写的箭头函数每次Board渲染都会重新创建,是memo失效的核心原因,直接改成事件委托的写法,所有Square共用同一个回调函数,通过DOM上的data属性拿方格索引:

import { useCallback } from 'react';
const Board = (props) => {
  // 用useCallback固定回调引用,依赖只有props.onClick
  const handleSquareClick = useCallback((e) => {
    const index = Number(e.target.dataset.index);
    props.onClick(index);
  }, [props.onClick]);

  const renderSquare = (i) => {
    return (
      <Square 
        value={props.square[i]} 
        onClick={handleSquareClick}
        data-index={i}
      />
    );
  };

  return (
    <div>
      <div className="border-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="border-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="border-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  );
};
export default Board;

3. 正确给Game组件的事件回调加缓存

之前空依赖的useCallback会导致闭包陷阱,把回调用到的所有状态都加入依赖数组,保证函数引用只在依赖变化时更新:

import { useState, useCallback, useMemo } from 'react';
const Game = () => {
  const [xIsNext, setXIsNext] = useState(true);
  const [stepNumber, setStepNumber] = useState(0);
  const [history, setHistory] = useState([{ squares: Array(9).fill(null) }]);
  
  const current = history[stepNumber];
  // 顺便把winner计算用useMemo缓存,避免每次渲染重复计算
  const winner = useMemo(() => CalculateWinner(current.squares), [current.squares]);

  // 缓存跳转步骤的回调
  const jumpTo = useCallback((step) => {
    setStepNumber(step);
    setXIsNext(step % 2 === 0);
  }, []);

  // 缓存历史步骤列表,避免每次渲染重新生成
  const move = useMemo(() => {
    return history.map((step, move) => {
      const desc = move ? `Go to # ${move}` : "Game Start";
      return (
        <div key={move}>
          <button className="btninfo" onClick={() => jumpTo(move)}>{desc}</button>
        </div>
      );
    });
  }, [history, jumpTo]);

  let status;
  if (winner) {
    status = `Winner is ${winner}`;
  } else {
    status = `Turn for Player ${xIsNext ? "X" : "O"}`;
  }

  // 正确写依赖的handleClick缓存
  const handleClick = useCallback((i) => {
    const latestHistory = history.slice(0, stepNumber + 1);
    const current = latestHistory[latestHistory.length - 1];
    const squares = current.squares.slice();
    const winner = CalculateWinner(squares);
    if (winner || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(history.concat({ squares: squares }));
    setXIsNext(!xIsNext);
    setStepNumber(history.length);
  }, [history, stepNumber, xIsNext]);

  const handleRestart = useCallback(() => {
    setXIsNext(true);
    setStepNumber(0);
    setHistory([{ squares: Array(9).fill(null) }]);
  }, []);

  return (
    <div className="game">
      <div className="game-board">
        <div className="game-status">{status}</div>
        <Board onClick={handleClick} square={current.squares} />
      </div>
      <div className="game-info">
        <button className="btninfo" onClick={handleRestart}>Restart</button>
        <div>{move}</div>
      </div>
    </div>
  );
};
export default Game;

// 胜负判断逻辑不用改
const CalculateWinner = (squares) => {
  const lines = [
    [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 i = 0; i < lines.length; i++) {
    const [a,b,c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[b] === squares[c]) {
      return squares[a];
    }
  }
  return null;
};

验证方式

打开React DevTools,勾选「Highlight updates when components render」选项,点击单个方格时,只会高亮当前点击的方格和顶部状态条,其余未修改的方格不会出现更新高亮,就说明优化生效了。

提示:井字棋本身节点极少,全量重渲染的性能损耗几乎可以忽略,这个优化的核心价值是理清React的渲染逻辑和缓存API的正确用法,不要在小型项目里为了缓存过度增加代码复杂度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 10:12:16