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
相关产品推荐
相关产品推荐

