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

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;

逻辑说明

  1. 搜索函数:递归遍历节点,匹配关键词后,叶子节点会触发所有父节点展开;非叶子节点仅保留自身及匹配的子节点,初始不展开。
  2. Node组件:区分自动展开(搜索触发)和手动展开状态,手动切换后优先使用手动状态,保证交互灵活性。
  3. 父组件:管理搜索输入、过滤数据和展开状态,合并自动与手动展开的节点集合,传递给子组件实现完整功能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 18:35:07