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

React JS中如何实现可动态新增行/子行的tree table

React 实现支持动态增行的Tree Table方案

核心数据结构

树形表格的渲染基础是嵌套数组结构,每一行必须包含全局唯一ID、业务字段、子节点数组三个核心部分,基础结构示例:

const tableRowStruct = {
  id: '全局唯一字符串',
  // 自定义业务字段,比如名称、数值、状态等
  name: '',
  value: '',
  // 子行数组,叶子节点该字段为空数组
  children: []
}

注意:所有行ID必须全局唯一,否则展开、增删操作会出现节点匹配错误

核心实现逻辑

  • 递归渲染行:单独封装可递归调用的行组件,根据当前行层级计算首列缩进值,根据子节点存在与否判断是否展示展开/收起按钮
  • 新增同级普通行:递归遍历数据找到当前点击行所在的父级数组,在当前行的下一位插入新的空白行数据
  • 新增子行:递归遍历找到当前点击行,往它的children数组末尾插入新的空白行,如果当前行是收起状态,新增后自动触发展开
  • 展开状态管理:用Set存储所有处于展开状态的行ID,点击展开/收起按钮时增删对应ID即可

最小可运行代码示例

import { useState } from 'react';
import './TreeTable.css';

// 生成全局唯一行ID,生产环境可替换为uuid等成熟方案
const genRowId = () => `row-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;

// 递归渲染的单行组件
function TreeRow({ row, level, onAddSibling, onAddChild, onToggleExpand, expandedIds }) {
  const hasChildren = row.children?.length > 0;
  const isExpanded = expandedIds.has(row.id);

  return (
    <>
      <tr className="tree-row">
        <td>
          <div className="tree-cell" style={{ paddingLeft: `${level * 24}px` }}>
            {/* 展开收起按钮 */}
            {hasChildren ? (
              <button
                className="expand-btn"
                onClick={() => onToggleExpand(row.id)}
              >
                {isExpanded ? '-' : '+'}
              </button>
            ) : (
              <span className="btn-placeholder" />
            )}
            <span className="row-text">{row.name}</span>
            {/* 增行操作按钮 */}
            <div className="action-group">
              <button
                className="add-btn"
                title="新增同级普通行"
                onClick={() => onAddSibling(row.id)}
              >
                + 同级
              </button>
              <button
                className="add-btn"
                title="新增子行"
                onClick={() => onAddChild(row.id)}
              >
                + 子行
              </button>
            </div>
          </div>
        </td>
        {/* 其余业务列按需扩展 */}
        <td>{row.value || '-'}</td>
      </tr>
      {/* 递归渲染子行,仅展开状态下显示 */}
      {hasChildren && isExpanded && row.children.map(child => (
        <TreeRow
          key={child.id}
          row={child}
          level={level + 1}
          onAddSibling={onAddSibling}
          onAddChild={onAddChild}
          onToggleExpand={onToggleExpand}
          expandedIds={expandedIds}
        />
      ))}
    </>
  );
}

export default function DynamicTreeTable() {
  const [tableData, setTableData] = useState([
    { id: genRowId(), name: '默认一级行', value: '示例数据', children: [] }
  ]);
  // 存储所有展开状态的行ID
  const [expandedIds, setExpandedIds] = useState(new Set());

  // 递归查找目标行、所属父数组及索引,供增删改操作调用
  const findNodeInfo = (list, targetId) => {
    for (let i = 0; i < list.length; i++) {
      const item = list[i];
      if (item.id === targetId) {
        return { target: item, parentList: list, index: i };
      }
      if (item.children?.length) {
        const res = findNodeInfo(item.children, targetId);
        if (res) return res;
      }
    }
    return null;
  };

  // 新增同级普通行
  const addSiblingRow = (targetId) => {
    const newRow = { id: genRowId(), name: '新增普通行', value: '', children: [] };
    setTableData(prev => {
      const dataCopy = JSON.parse(JSON.stringify(prev));
      const { parentList, index } = findNodeInfo(dataCopy, targetId);
      parentList.splice(index + 1, 0, newRow);
      return dataCopy;
    });
  };

  // 新增子行
  const addChildRow = (targetId) => {
    const newRow = { id: genRowId(), name: '新增子行', value: '', children: [] };
    setTableData(prev => {
      const dataCopy = JSON.parse(JSON.stringify(prev));
      const { target } = findNodeInfo(dataCopy, targetId);
      target.children.push(newRow);
      return dataCopy;
    });
    // 新增子行后自动展开当前节点
    setExpandedIds(prev => new Set([...prev, targetId]));
  };

  // 切换节点展开/收起状态
  const toggleExpand = (rowId) => {
    setExpandedIds(prev => {
      const newSet = new Set(prev);
      newSet.has(rowId) ? newSet.delete(rowId) : newSet.add(rowId);
      return newSet;
    });
  };

  return (
    <table className="tree-table" border="1" cellPadding="8" cellSpacing="0">
      <thead>
        <tr>
          <th width="400">名称</th>
          <th>数值</th>
        </tr>
      </thead>
      <tbody>
        {tableData.map(row => (
          <TreeRow
            key={row.id}
            row={row}
            level={0}
            onAddSibling={addSiblingRow}
            onAddChild={addChildRow}
            onToggleExpand={toggleExpand}
            expandedIds={expandedIds}
          />
        ))}
      </tbody>
    </table>
  );
}

配套基础样式

.tree-table {
  width: 100%;
  border-collapse: collapse;
}
.tree-cell {
  display: flex;
  align-items: center;
  gap: 8px;
}
.expand-btn {
  width: 20px;
  height: 20px;
  border: 1px solid #d9d9d9;
  border-radius: 2px;
  background: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.btn-placeholder {
  width: 20px;
  height: 20px;
}
.action-group {
  margin-left: auto;
  display: flex;
  gap: 6px;
}
.add-btn {
  border: 1px solid #1677ff;
  background: #e6f4ff;
  color: #1677ff;
  border-radius: 4px;
  padding: 2px 6px;
  cursor: pointer;
  font-size: 12px;
}
.add-btn:hover {
  background: #1677ff;
  color: #fff;
}

可选扩展方向

  • 数据量超过1000条时,递归渲染可能出现性能问题,可搭配虚拟滚动库优化,或基于react-table、组件库Table等成熟组件的树形能力二次开发
  • 新增行后可自动聚焦到行内输入框,省去手动点击步骤
  • 可扩展行删除、拖拽排序、行内编辑功能,核心逻辑和增行一致,都是通过递归找到目标节点后操作数组即可
  • 可添加层级连接线、自定义展开图标,对齐目标设计效果

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 08:46:34