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

React树形组件中无关节点不必要重渲染问题求助

React TreeView组件不必要重渲染优化

问题背景

实现层级文件结构的TreeView组件,支持节点展开/折叠。因需要全局展开/折叠功能,将状态提升至父组件后,出现性能问题:每次切换单个节点的展开状态时,所有节点都会触发不必要的重渲染,即使节点本身的状态未受影响。

简化代码如下:

import { useState } from "react";

export default function TreeView({ data }) {
  const [expandedNodes, setExpandedNodes] = useState(new Set());

  const toggleNode = (nodeId) => {
    setExpandedNodes((prev) => {
      const next = new Set(prev);
      if (next.has(nodeId)) next.delete(nodeId);
      else next.add(nodeId);
      return next;
    });
  };

  return (
    <ul>
      {data.map((node) => (
        <TreeNode
          key={node.id}
          node={node}
          expandedNodes={expandedNodes}
          isExpanded={expandedNodes.has(node.id)}
          onToggle={toggleNode}
        />
      ))}
    </ul>
  );
}

function TreeNode({ node, isExpanded, onToggle, expandedNodes }) {
  return (
    <li>
      <div onClick={() => onToggle(node.id)}>
        {node.name} {isExpanded ? "[-]" : "[+]"}
      </div>
      <ul>
        {isExpanded &&
          node.children &&
          node.children.map((n) => (
            <TreeNode
              key={n.id}
              node={n}
              isExpanded={expandedNodes.has(n.id)}
              expandedNodes={expandedNodes}
              onToggle={onToggle}
            />
          ))}
      </ul>
    </li>
  );
}

文件结构示例:

Root
├── Documents
└── Pictures
    ├── Vacation
    └── Work

例如展开“Pictures”时,“Documents”节点也会重渲染,而它的状态并未改变。尝试过React.memo优化,但expandedNodes每次更新都会生成新的Set实例,导致所有TreeNode的props引用变化,memo失效。

核心原因

每次切换节点时,setExpandedNodes会返回新的Set实例,导致所有TreeNode组件接收的expandedNodes props引用更新,即使节点自身的展开状态未变,也会触发重渲染。

优化方案

1. 稳定回调函数引用

用useCallback包裹toggleNode,避免父组件重渲染时创建新的函数实例,保证onToggle props的稳定性。

2. 避免传递整个Set集合

只给TreeNode传递当前节点的isExpanded布尔值,而非整个expandedNodes集合,减少props的引用变化。

3. React.memo配合自定义比较逻辑

用memo包裹TreeNode组件,自定义比较函数仅关注节点ID和当前展开状态,忽略其他引用类型props的变化。

4. 递归渲染时传递状态判断函数

通过useCallback创建稳定的isNodeExpanded函数,用于递归获取子节点的展开状态,避免传递整个Set。

修改后的完整代码

import { useState, useCallback, memo } from "react";

export default function TreeView({ data }) {
  const [expandedNodes, setExpandedNodes] = useState(new Set());

  // 稳定toggle函数引用
  const toggleNode = useCallback((nodeId) => {
    setExpandedNodes((prev) => {
      const next = new Set(prev);
      next.has(nodeId) ? next.delete(nodeId) : next.add(nodeId);
      return next;
    });
  }, []);

  // 稳定节点展开状态判断函数
  const isNodeExpanded = useCallback((nodeId) => {
    return expandedNodes.has(nodeId);
  }, [expandedNodes]);

  const renderTreeNode = (node) => {
    return (
      <TreeNode
        key={node.id}
        node={node}
        isExpanded={isNodeExpanded(node.id)}
        isNodeExpanded={isNodeExpanded}
        onToggle={toggleNode}
      />
    );
  };

  return (
    <ul>
      {data.map(renderTreeNode)}
    </ul>
  );
}

// 用memo包裹并自定义比较逻辑
const TreeNode = memo(({ node, isExpanded, onToggle, isNodeExpanded }) => {
  return (
    <li>
      <div onClick={() => onToggle(node.id)}>
        {node.name} {isExpanded ? "[-]" : "[+]"}
      </div>
      {isExpanded && node.children && (
        <ul>
          {node.children.map((child) => (
            <TreeNode
              key={child.id}
              node={child}
              isExpanded={isNodeExpanded(child.id)}
              isNodeExpanded={isNodeExpanded}
              onToggle={onToggle}
            />
          ))}
        </ul>
      )}
    </li>
  );
}, (prevProps, nextProps) => {
  // 仅比较节点ID和展开状态,忽略其他props的引用变化
  return (
    prevProps.node.id === nextProps.node.id &&
    prevProps.isExpanded === nextProps.isExpanded
  );
});

优化效果

修改后,切换单个节点时,只有该节点及其子节点会触发重渲染,其他未受影响的节点将跳过不必要的渲染,大幅提升组件性能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 02:35:57