实现树形组件搜索时展开匹配节点且保留完整搜索功能
树形组件搜索筛选与展开状态控制实现方案
需求梳理
- 搜索输入时,所有匹配的节点需保持展开状态
- 清空搜索内容后,树形组件恢复初始折叠/展开状态
- 全树搜索逻辑:
- 若父节点匹配搜索关键词,仅展开至该父节点层级,其子节点可维持折叠状态
- 若子节点匹配,需自动展开所有祖先节点路径,确保匹配的子节点可见
完整实现代码
主组件 TreeList.js
import React, { useEffect, useState } from "react"; import Tree from "./Tree"; import { data } from "./data"; import "./styles.css"; const TreeList = () => { const [search, setSearch] = useState(""); // 存储初始展开状态,用于清空搜索时恢复 const [initialExpandedKeys, setInitialExpandedKeys] = useState([]); useEffect(() => { // 初始化时记录初始展开状态(可根据业务自定义初始展开节点) setInitialExpandedKeys([]); }, []); return ( <> <input type="text" value={search} placeholder="搜索节点" onChange={(e) => setSearch(e.target.value.trim())} /> <Tree data={data} searchKeyword={search} initialExpandedKeys={initialExpandedKeys} /> </> ); }; export default TreeList;
树形节点组件 Tree.js
import React, { useState, useMemo } from "react"; const Tree = ({ data, searchKeyword, initialExpandedKeys }) => { const [expandedKeys, setExpandedKeys] = useState(initialExpandedKeys); // 清空搜索时恢复初始状态 useState(() => { if (!searchKeyword) { setExpandedKeys(initialExpandedKeys); } }, [searchKeyword, initialExpandedKeys]); // 递归判断节点是否匹配搜索关键词,或后代节点是否匹配 const checkMatch = (node, keyword) => { if (!keyword) return false; // 当前节点匹配 if (node.name.toLowerCase().includes(keyword.toLowerCase())) { return true; } // 递归检查子节点 if (node.children && node.children.length > 0) { return node.children.some(child => checkMatch(child, keyword)); } return false; }; // 递归获取需要展开的节点key集合 const getExpandedKeys = (nodes, keyword, parentKeys = []) => { let keys = []; nodes.forEach(node => { const currentPath = [...parentKeys, node.id]; // 当前节点匹配,仅加入当前节点key if (node.name.toLowerCase().includes(keyword.toLowerCase())) { keys.push(node.id); } else if (node.children && node.children.length > 0) { // 子节点匹配,加入所有祖先节点key const childMatches = getExpandedKeys(node.children, keyword, currentPath); if (childMatches.length > 0) { keys = [...keys, ...currentPath]; } } }); // 去重并返回 return [...new Set(keys)]; }; // 根据搜索关键词计算需要展开的节点 const computedExpandedKeys = useMemo(() => { if (!searchKeyword) return initialExpandedKeys; return getExpandedKeys(data, searchKeyword); }, [searchKeyword, data, initialExpandedKeys]); // 切换节点展开状态(非搜索模式下的手动操作) const toggleExpand = (nodeId) => { if (searchKeyword) return; // 搜索模式下禁止手动修改展开状态 setExpandedKeys(prev => prev.includes(nodeId) ? prev.filter(id => id !== nodeId) : [...prev, nodeId] ); }; // 递归渲染树形节点 const renderTreeNodes = (nodes) => { return nodes.map(node => { const isMatch = checkMatch(node, searchKeyword); const isExpanded = computedExpandedKeys.includes(node.id); const hasChildren = node.children && node.children.length > 0; return ( <div key={node.id} className="tree-node"> <div className={`node-header ${isMatch ? 'matched' : ''}`} onClick={() => toggleExpand(node.id)} > {hasChildren && ( <span className="expand-icon">{isExpanded ? '▼' : '▶'}</span> )} {node.name} </div> {isExpanded && hasChildren && ( <div className="node-children"> {renderTreeNodes(node.children)} </div> )} </div> ); }); }; return <div className="tree-container">{renderTreeNodes(data)}</div>; }; export default Tree;
示例数据 data.js
export const data = [ { id: 1, name: "父节点1", children: [ { id: 11, name: "子节点1-1" }, { id: 12, name: "子节点1-2", children: [{ id: 121, name: "孙节点1-2-1" }] } ] }, { id: 2, name: "父节点2", children: [ { id: 21, name: "子节点2-1" }, { id: 22, name: "子节点2-2" } ] } ];
样式文件 styles.css
.tree-container { margin-top: 16px; padding-left: 8px; } .tree-node { margin: 4px 0; } .node-header { display: flex; align-items: center; cursor: pointer; padding: 4px; } .expand-icon { margin-right: 4px; font-size: 12px; } .node-children { padding-left: 20px; } .matched { background-color: #f0f8ff; font-weight: bold; } input { padding: 8px; width: 300px; border: 1px solid #ddd; border-radius: 4px; }
实现说明
- 搜索匹配与展开控制:
- 通过
checkMatch递归判断节点自身或后代是否匹配关键词 getExpandedKeys根据匹配规则生成需要展开的节点ID集合:父节点匹配仅加入自身ID,子节点匹配则加入所有祖先节点ID
- 通过
- 状态恢复:
- 组件初始化时记录初始展开状态,清空搜索时自动重置
expandedKeys为初始值
- 组件初始化时记录初始展开状态,清空搜索时自动重置
- 交互限制:
- 搜索模式下禁止手动修改展开状态,避免与搜索自动展开逻辑冲突
- 性能优化:
- 使用
useMemo缓存计算后的展开节点集合,避免每次渲染重复计算
- 使用
内容的提问来源于stack exchange,提问作者user16860065
相关产品推荐
相关产品推荐

