RTKQ场景下React树形Selector组件按需加载子节点架构咨询
可行架构方案推荐
方案一:用RTKQ useLazyQuery 实现按需加载,结合组件状态管理
核心思路是利用RTKQ的useLazyQuery钩子(允许在事件回调中触发请求),配合组件内部状态记录已加载的节点数据和展开状态,完全规避useSelector违反顶层调用规则的问题。
实现步骤:
- 适配异步场景调整类型(可选):
// 将同步函数类型扩展为异步,适配API请求场景 export type SelectorDataFn = (parent?: SelectorItem) => Promise<SelectorItem[]>; export type SelectorData = SelectorDataFn | SelectorItem[];
- 组件内整合逻辑:
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请求,自然实现按需加载。
实现步骤:
- 创建独立节点组件:
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> ); };
- 主组件中集成节点组件:
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
相关产品推荐
相关产品推荐

