ReactJS递归渲染树形结构的全局搜索功能实现求助
树形结构递归搜索功能实现需求
我通过对象数组递归渲染了树形结构,已实现展开/折叠功能。现需添加输入框递归搜索功能,要求:
- 匹配叶子节点时,展开显示该节点的所有父节点
- 匹配中间或根节点时,仅显示到该节点(初始折叠,可手动展开)
以下是已实现的Node组件代码及尝试但无效的searchTree函数:
Node组件代码
import React, { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export const Node = ({ node, level, currentActive, setCurrentActive }) => { const [isOpen, setIsOpen] = useState(false); const hasChildren = !!node?.categories?.length; const categoryType = level === 0 ? "category" : !hasChildren ? "leaf" : "group"; if (!node) return null; const { name, key, categories } = node; const activeClassName = currentActive === name ? "active" : ""; return ( <> <div className={`list-row level-${level} ${categoryType} ${activeClassName}`} onClick={() => { setIsOpen((open) => !open); if (!hasChildren) { setCurrentActive((prevName) => (!(prevName === name) ? name : "")); } }} key={key} > <div className="list-item-holder"> {hasChildren && ( <div className="list-item-expander-holder"> <span className={`expand-collapse-icon ${isOpen ? "collapse" : "expand"}`} > <span className="expand-icon"> <FontAwesomeIcon icon="caret-down" /> </span> <span className="collapse-icon"> <FontAwesomeIcon icon="caret-right" /> </span> </span> </div> )} <div className="list-item-details-holder">{name}</div> </div> </div> {isOpen && hasChildren && ( <div className="list-row-children"> {categories.map((node) => ( <Node key={node.key} node={node} level={level + 1} currentActive={currentActive} setCurrentActive={setCurrentActive} /> ))} </div> )} </> ); };
尝试的无效搜索函数
function searchTree(tree, query) { function traverse(node, query) { if (node.categories) { traverse(node.categories, query); } return { ...node, categories: node?.categories?.filter(({ name }) => name === query) }; } return tree.map((root) => traverse(root, query)); }
解决方案
1. 实现正确的搜索处理函数
该函数会返回过滤后的树形结构,同时收集需要展开的节点键集合,用于控制父节点展开:
// 递归查找匹配节点,收集需展开的父节点键 function findMatchesAndExpandedNodes(node, query, parentKeys = [], expandedKeys = new Set()) { const lowerQuery = query.toLowerCase(); const nodeNameLower = node.name.toLowerCase(); let hasMatch = nodeNameLower.includes(lowerQuery); let filteredChildren = []; if (node.categories?.length) { for (const child of node.categories) { const { child: filteredChild, hasMatch: childHasMatch } = findMatchesAndExpandedNodes( child, query, [...parentKeys, node.key], expandedKeys ); if (childHasMatch) { filteredChildren.push(filteredChild); hasMatch = true; // 子节点匹配时,当前节点需展开 expandedKeys.add(node.key); } } } // 叶子节点匹配时,所有父节点都要展开 if (!node.categories?.length && hasMatch) { parentKeys.forEach(key => expandedKeys.add(key)); } return { child: { ...node, categories: filteredChildren }, hasMatch, expandedKeys }; } // 对外的搜索入口函数 function searchTree(tree, query) { if (!query.trim()) return { filteredTree: tree, expandedKeys: new Set() }; const filteredTree = []; const expandedKeys = new Set(); for (const root of tree) { const { child: filteredRoot, hasMatch } = findMatchesAndExpandedNodes(root, query, [], expandedKeys); if (hasMatch) { filteredTree.push(filteredRoot); } } return { filteredTree, expandedKeys }; }
2. 修改Node组件,支持外部控制展开状态
将内部isOpen状态替换为外部传入的展开控制,同时保留手动切换能力:
import React, { useState, useMemo } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export const Node = ({ node, level, currentActive, setCurrentActive, expandedKeys, // 外部传入的展开节点集合 onToggleExpand // 手动切换展开的回调 }) => { // 手动切换的展开状态,优先级高于外部自动展开 const [manualOpen, setManualOpen] = useState(null); const hasChildren = !!node?.categories?.length; const categoryType = level === 0 ? "category" : !hasChildren ? "leaf" : "group"; if (!node) return null; const { name, key, categories } = node; const activeClassName = currentActive === name ? "active" : ""; // 最终展开状态:手动切换过则用手动状态,否则用外部自动展开状态 const isOpen = useMemo(() => { return manualOpen !== null ? manualOpen : expandedKeys.has(key); }, [manualOpen, expandedKeys, key]); const handleToggle = () => { const newOpen = !isOpen; setManualOpen(newOpen); onToggleExpand?.(key, newOpen); }; return ( <> <div className={`list-row level-${level} ${categoryType} ${activeClassName}`} onClick={() => { hasChildren ? handleToggle() : setCurrentActive(prev => prev === name ? "" : name); }} key={key} > <div className="list-item-holder"> {hasChildren && ( <div className="list-item-expander-holder"> <span className={`expand-collapse-icon ${isOpen ? "collapse" : "expand"}`} > <span className="expand-icon"> <FontAwesomeIcon icon="caret-down" /> </span> <span className="collapse-icon"> <FontAwesomeIcon icon="caret-right" /> </span> </span> </div> )} <div className="list-item-details-holder">{name}</div> </div> </div> {isOpen && hasChildren && ( <div className="list-row-children"> {categories.map(childNode => ( <Node key={childNode.key} node={childNode} level={level + 1} currentActive={currentActive} setCurrentActive={setCurrentActive} expandedKeys={expandedKeys} onToggleExpand={onToggleExpand} /> ))} </div> )} </> ); };
3. 父组件集成搜索功能
在父组件中管理搜索输入、过滤后的树数据及展开状态:
import React, { useState, useMemo } from "react"; import { Node } from "./Node"; import treeData from "./treeData"; // 替换为你的原始树形数据 const TreeContainer = () => { const [searchQuery, setSearchQuery] = useState(""); const [currentActive, setCurrentActive] = useState(""); const [manualExpandedKeys, setManualExpandedKeys] = useState(new Set()); // 计算过滤后的树和自动展开节点 const { filteredTree, expandedKeys: autoExpandedKeys } = useMemo(() => { return searchTree(treeData, searchQuery); }, [searchQuery]); // 合并自动展开与手动展开的节点集合 const mergedExpandedKeys = useMemo(() => { return new Set([...autoExpandedKeys, ...manualExpandedKeys]); }, [autoExpandedKeys, manualExpandedKeys]); const handleToggleExpand = (key, isOpen) => { const newManualKeys = new Set(manualExpandedKeys); isOpen ? newManualKeys.add(key) : newManualKeys.delete(key); setManualExpandedKeys(newManualKeys); }; return ( <div> <input type="text" value={searchQuery} onChange={(e) => { setSearchQuery(e.target.value); // 搜索时重置手动展开状态,回到自动展开逻辑 setManualExpandedKeys(new Set()); }} placeholder="搜索节点..." /> {filteredTree.map(rootNode => ( <Node key={rootNode.key} node={rootNode} level={0} currentActive={currentActive} setCurrentActive={setCurrentActive} expandedKeys={mergedExpandedKeys} onToggleExpand={handleToggleExpand} /> ))} </div> ); }; export default TreeContainer;
逻辑说明
- 搜索函数:递归遍历节点,匹配关键词后,叶子节点会触发所有父节点展开;非叶子节点仅保留自身及匹配的子节点,初始不展开。
- Node组件:区分自动展开(搜索触发)和手动展开状态,手动切换后优先使用手动状态,保证交互灵活性。
- 父组件:管理搜索输入、过滤数据和展开状态,合并自动与手动展开的节点集合,传递给子组件实现完整功能。
内容的提问来源于stack exchange,提问作者lrr59
相关产品推荐
相关产品推荐

