React树形组件:父节点折叠时如何收起所有展开子节点?
React树形结构折叠修复方案

我基于React开发了XML转树形结构的功能,本地Visual Studio运行正常,但在线Sandbox环境中报错(不熟悉该环境报错逻辑)。当前树形结构可正常展开,但存在问题:父节点折叠时,已展开的子节点不会随之收起,需要修改代码解决该问题。
原代码
App.js
import React, { Component } from "react"; import ProductsTree from "./ProductsTreeView"; const initialState = { currentNode: {}, data: "" }; export class App extends Component { constructor(props) { super(props); this.state = initialState; this.setCurrentNode = this.setCurrentNode.bind(this); } setCurrentNode(node) { this.setState({ currentNode: node }); } render() { return ( <div> <ProductsTree setCurrentNode={this.setCurrentNode} /> </div> ); } } export default App;
ProductsTreeView.js
import React, { Component } from "react"; import axios from "axios"; import XMLParser from "react-xml-parser"; import { Link } from "react-router-dom"; import tree from "./tree.xml"; class ProductsTreeView extends Component { render() { return ( <div id="TreeView"> <TreeView setCurrentNode={this.props.setCurrentNode} /> </div> ); } } class Node { description = "n/a"; id = -1; key_id = -1; linkpagename = ""; isActive = false; nodes = []; constructor(description, id, key_id, linkpagename) { this.description = description; this.id = id; this.key_id = key_id; this.linkpagename = linkpagename; } static nodesFromXml(xml) { const map = (entity, nodes) => { const id = entity.attributes["id"]; const key_id = entity.attributes["key-id"]; const descriptionText = entity.children[ entity.children.findIndex((child) => child.name === "description") ].value; const entities = entity.children.filter( (child) => child.name === "entity" ); var linkPageName = entity.attributes["link-page-name"]; linkPageName = linkPageName.replace(".aspx", ""); const node = new Node(descriptionText, id, key_id, linkPageName); nodes.push(node); entities.forEach((entity) => map(entity, node.nodes)); }; const parsedData = new XMLParser().parseFromString(xml); const entities = parsedData.children.filter( (child) => child.name === "entity" ); const nodes = []; entities.forEach((entity) => map(entity, nodes)); return nodes; } } class TreeView extends React.Component { constructor(props) { super(props); this.state = { nodes: [] }; this.toggleNode = this.toggleNode.bind(this); } componentDidMount() { axios .get(tree, { "Content-Type": "application/xml; charset=utf-8" }) .then((response) => this.setState({ nodes: Node.nodesFromXml(response.data) }) ) .catch((error) => console.error("Error:", error)); } render() { const nodes = this.state.nodes return ( <ul> {nodes.map((node) => ( <TreeNode id={node.id} key={node.key_id} node={node} onToggle={this.toggleNode} setCurrentNode={this.props.setCurrentNode} /> ))} </ul> ); } toggleNode(node) { this.props.setCurrentNode(node); function _toggleNode(currentNode, node) { if (currentNode.id === node.id) { if (currentNode.key_id === node.key_id) { currentNode.isActive = !currentNode.isActive; } } else { currentNode.nodes.forEach((childNode) => _toggleNode(childNode, node)); } } const nodes = this.state.nodes; nodes.forEach((currentNode) => _toggleNode(currentNode, node)); this.setState((state) => (state.nodes = nodes)); } } class TreeNode extends React.Component { render() { const node = this.props.node; const onToggle = this.props.onToggle; let activeChildren = null; if (node.isActive && node.nodes.length > 0) { activeChildren = ( <ul> {node.nodes.map((node) => ( <TreeNode id={node.id} key={node.key_id} node={node} onToggle={onToggle} /> ))} </ul> ); } return ( <li id={node.id} linkpagename={node.linkpagename} key={node.key_id} onClick={(event) => { event.stopPropagation(); onToggle(node); }} > <Link to={node.linkpagename} style={{ textDecoration: "none", color: "#000000" }} > {node.description} </Link>{" "} - {node.key_id} - {node.linkpagename} {activeChildren} </li> ); } } export default ProductsTreeView;
tree.xml
<?xml version="1.0" standalone="yes"?> <tree> <entity id="e11" key-id="1" link-page-name="Add_Category"> <description>Service</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e248" key-id="48" link-page-name="Edit_Category"> <description>A_test1_test1</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e3717" key-id="717" link-page-name="Edit_Product"> <description>A_SubItem1</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e45546" key-id="5546" link-page-name="Edit_ProdTemplate"> <description>A_Test_Template</description> <image>images/paper.gif</image> <imageOpen>images/paper.gif</imageOpen> </entity> </entity> </entity> <entity id="e247" key-id="47" link-page-name="Edit_Category"> <description>A_test6</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e3716" key-id="716" link-page-name="Edit_Product"> <description>A_Item</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e45545" key-id="5545" link-page-name="Edit_ProdTemplate"> <description>temp1</description> <image>images/paper.gif</image> <imageOpen>images/paper.gif</imageOpen> </entity> </entity> </entity> </entity> <entity id="e12" key-id="2" link-page-name="Add_Category"> <description>Sales</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e230" key-id="30" link-page-name="Edit_Category"> <description>Gift Cards</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e3421" key-id="421" link-page-name="Edit_Product"> <description>Sample Card</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e43308" key-id="3308" link-page-name="Edit_ProdTemplate"> <description>greeting temp</description> <image>images/paper.gif</image> <imageOpen>images/paper.gif</imageOpen> </entity> </entity> <entity id="e3422" key-id="422" link-page-name="Edit_Product"> <description>De Card</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e43309" key-id="3309" link-page-name="Edit_ProdTemplate"> <description>NS Temp</description> <image>images/paper.gif</image> <imageOpen>images/paper.gif</imageOpen> </entity> </entity> </entity> <entity id="e215" key-id="15" link-page-name="Edit_Category"> <description>Chck</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e3671" key-id="671" link-page-name="Edit_Product"> <description>Add item</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e45438" key-id="5438" link-page-name="Edit_ProdTemplate"> <description>Ahhhh</description> <image>images/paper.gif</image> <imageOpen>images/paper.gif</imageOpen> </entity> </entity> <entity id="e3450" key-id="450" link-page-name="Edit_Product"> <description>Advtttt</description> <image>images/plus.gif</image> <imageNode>images/de.gif</imageNode> <imageOpen>images/minus.gif</imageOpen> <entity id="e43577" key-id="3577" link-page-name="Edit_ProdTemplate"> <description>gggggg</description> <image>images/paper.gif</image> <imageOpen>images/paper.gif</imageOpen> </entity> </entity> </entity> </entity> </tree>
修复方案
核心问题:当前仅切换了点击节点的isActive状态,未在父节点折叠时递归重置所有子节点的isActive状态。
1. 修改TreeView的toggleNode函数
替换原toggleNode方法,新增递归收起子节点的逻辑:
toggleNode(node) { this.props.setCurrentNode(node); // 递归重置所有子节点为折叠状态 function collapseChildren(currentNode) { currentNode.isActive = false; currentNode.nodes.forEach(child => collapseChildren(child)); } function _toggleNode(currentNode, node) { if (currentNode.id === node.id && currentNode.key_id === node.key_id) { currentNode.isActive = !currentNode.isActive; // 若当前节点变为折叠状态,同步收起所有子节点 if (!currentNode.isActive) { currentNode.nodes.forEach(child => collapseChildren(child)); } } else { currentNode.nodes.forEach((childNode) => _toggleNode(childNode, node)); } } // 创建数组副本,遵循React状态不可变原则 const nodes = [...this.state.nodes]; nodes.forEach((currentNode) => _toggleNode(currentNode, node)); this.setState({ nodes }); }
2. 补充TreeNode的setCurrentNode传递
在TreeNode组件的子节点渲染中添加setCurrentNode属性,确保所有节点点击都能更新父组件状态:
// 在TreeNode的map渲染中修改 <TreeNode id={node.id} key={node.key_id} node={node} onToggle={onToggle} setCurrentNode={this.props.setCurrentNode} // 新增该行 />
说明
- 新增
collapseChildren函数,递归将所有子节点的isActive设为false,保证父节点折叠时所有展开的子节点同步收起 - 修改
setState写法,创建数组副本而非直接修改原数组,符合React状态更新的不可变原则 - 补充子节点的
setCurrentNode传递,修复子节点点击无法更新父组件currentNode的问题
内容的提问来源于stack exchange,提问作者TestUser
相关产品推荐
相关产品推荐

