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

RTKQ场景下React树形Selector组件按需加载子节点架构咨询

可行架构方案推荐

方案一:用RTKQ useLazyQuery 实现按需加载,结合组件状态管理

核心思路是利用RTKQ的useLazyQuery钩子(允许在事件回调中触发请求),配合组件内部状态记录已加载的节点数据和展开状态,完全规避useSelector违反顶层调用规则的问题。

实现步骤:

  1. 适配异步场景调整类型(可选):
// 将同步函数类型扩展为异步,适配API请求场景
export type SelectorDataFn = (parent?: SelectorItem) => Promise<SelectorItem[]>;
export type SelectorData = SelectorDataFn | SelectorItem[];
  1. 组件内整合逻辑:
import { useLazyQuery } from '@reduxjs/toolkit/query/react';
import { useState, useMemo, useEffect } from 'react';
import { fetchChildren, fetchRootItems } from './yourApiSlice'; // 你的RTKQ查询接口

export const Selector = ({ rootItems, icon, title, onCancel }: Props) => {
  // 缓存已加载的子节点:key为父节点ID,value为子节点数组
  const [loadedChildren, setLoadedChildren] = useState<Record<string, SelectorItem[]>>({});
  // 记录展开状态的节点ID集合
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
  // RTKQ懒加载钩子,返回请求触发函数和加载状态
  const [fetchChildNodes, { isLoading }] = useLazyQuery(fetchChildren);
  // 根节点数据状态
  const [renderRootItems, setRenderRootItems] = useState<SelectorItem[]>([]);

  // 初始化加载根节点数据
  useEffect(() => {
    const loadRoot = async () => {
      let items: SelectorItem[];
      if (Array.isArray(rootItems)) {
        items = rootItems;
      } else {
        items = await rootItems();
      }
      setRenderRootItems(items);
    };
    loadRoot();
  }, [rootItems]);

  // 节点展开/收缩处理
  const handleToggleExpand = async (parentItem: SelectorItem) => {
    const id = parentItem.id;
    if (expandedIds.has(id)) {
      // 收缩节点
      setExpandedIds(prev => new Set([...prev].filter(itemId => itemId !== id)));
      return;
    }

    // 标记节点为展开
    setExpandedIds(prev => new Set([...prev, id]));
    // 已加载过则直接返回
    if (loadedChildren[id]) return;

    // 触发RTKQ请求获取子节点
    const response = await fetchChildNodes({ parentId: id }).unwrap();
    // 缓存加载结果
    setLoadedChildren(prev => ({ ...prev, [id]: response }));
  };

  // 递归渲染节点的函数
  const renderNodeTree = (items: SelectorItem[]) => {
    return items.map(item => (
      <div key={item.id} className="selector-node">
        <div onClick={() => handleToggleExpand(item)} className="node-header">
          {expandedIds.has(item.id) ? '▼' : '▶'} {item.description}
          {isLoading && item.id === currentLoadingId && <span className="loading">加载中...</span>}
        </div>
        {/* 仅当节点展开且已加载子节点时渲染子树 */}
        {expandedIds.has(item.id) && loadedChildren[item.id] && (
          <div className="node-children" style={{ marginLeft: 24 }}>
            {renderNodeTree(loadedChildren[item.id])}
          </div>
        )}
      </div>
    ));
  };

  return (
    <div className="selector-container">
      <div className="selector-header">
        <span>{icon}</span>
        <h3>{title}</h3>
        {onCancel && <button onClick={onCancel} className="cancel-btn">取消</button>}
      </div>
      <div className="selector-content">
        {renderNodeTree(renderRootItems)}
      </div>
    </div>
  );
};

优势:

  • 完全符合React Hooks规则,所有钩子均在组件顶层调用
  • 复用RTKQ的自动缓存机制,重复展开节点不会触发重复请求
  • 组件内部状态职责清晰,分别管理展开状态和已加载数据

方案二:拆分节点为独立组件,用RTKQ useQuery 按需触发请求

核心思路是将单个节点拆分为独立的SelectorNode组件,让每个节点组件在顶层调用useQuery,通过enabled参数控制请求时机——仅当节点展开时才触发API请求,自然实现按需加载。

实现步骤:

  1. 创建独立节点组件:
import { useQuery } from '@reduxjs/toolkit/query/react';
import { useState } from 'react';
import { fetchChildren } from './yourApiSlice';

interface SelectorNodeProps {
  item: SelectorItem;
}

export const SelectorNode = ({ item }: SelectorNodeProps) => {
  const [isExpanded, setIsExpanded] = useState(false);
  // 仅当节点展开时才触发查询(enabled参数控制)
  const { data: children, isLoading } = useQuery(
    ['nodeChildren', item.id], // 缓存key,确保唯一
    () => fetchChildren({ parentId: item.id }).unwrap(),
    { enabled: isExpanded }
  );

  return (
    <div className="selector-node">
      <div onClick={() => setIsExpanded(!isExpanded)} className="node-header">
        {isExpanded ? '▼' : '▶'} {item.description}
        {isLoading && <span className="loading">加载中...</span>}
      </div>
      {isExpanded && children && (
        <div className="node-children" style={{ marginLeft: 24 }}>
          {children.map(child => (
            <SelectorNode key={child.id} item={child} />
          ))}
        </div>
      )}
    </div>
  );
};
  1. 主组件中集成节点组件:
import { useState, useEffect } from 'react';
import { SelectorNode } from './SelectorNode';

export const Selector = ({ rootItems, icon, title, onCancel }: Props) => {
  const [rootNodes, setRootNodes] = useState<SelectorItem[]>([]);

  // 初始化加载根节点
  useEffect(() => {
    const loadRoot = async () => {
      let items: SelectorItem[];
      if (Array.isArray(rootItems)) {
        items = rootItems;
      } else {
        items = await rootItems();
      }
      setRootNodes(items);
    };
    loadRoot();
  }, [rootItems]);

  return (
    <div className="selector-container">
      <div className="selector-header">
        <span>{icon}</span>
        <h3>{title}</h3>
        {onCancel && <button onClick={onCancel} className="cancel-btn">取消</button>}
      </div>
      <div className="selector-content">
        {rootNodes.map(item => (
          <SelectorNode key={item.id} item={item} />
        ))}
      </div>
    </div>
  );
};

优势:

  • 组件职责单一,SelectorNode专注于单个节点的渲染与数据加载
  • 完全遵循Hooks规则,useQuery在组件顶层调用,通过enabled精准控制请求时机
  • 递归渲染天然适配树形结构,代码简洁易维护
  • RTKQ自动处理缓存与请求去重,无需手动管理缓存状态

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 02:10:51