React中useSWR多请求致数据undefined的问题与优化咨询
问题描述
我正在开发React组件ClinicalViewsSummary,使用useSWR hook获取患者就诊(encounter)数据。子组件EncounterValuesTile会根据自身参数请求数据,但有时会收到undefined值——即使参数未变更且数据确实存在。经调研,不建议在底层组件中使用hook做数据请求,但当前场景无法避免。由于循环渲染,hook会在之前的请求完成前多次调用,导致部分数据为undefined。
主组件代码
const ClinicalViewsSummary: React.FC<OverviewListProps> = ({ patientUuid }) => { const config = useConfig(); const { t } = useTranslation(); const tilesDefinitions1 = config.tilesDefinitions; const tilesDefinitions = useMemo( () => [ { tileHeader: 'characteristicsTitle', columns: [ { id: 'currentRegimen', hasSummary: true, title: 'currentRegimenTitle', encounterType: 'e22e39fd-7db2-45e7-80f1-60fa0d5a4378', concept: '164515AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', summaryConcept: { primaryConcept: '164515AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', }, }, { id: 'artCohort', title: 'ArtCohortTitle', encounterType: 'e22e39fd-7db2-45e7-80f1-60fa0d5a4378', concept: '159599AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', isDate: true, conceptMappings: [ '159599AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '162572AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '164516AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '164431AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '160738AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ], }, { id: 'dsdModel', title: 'dSDModelTitle', hasSummary: true, encounterType: 'e22e39fd-7db2-45e7-80f1-60fa0d5a4378', concept: 'dfbe256e-30ba-4033-837a-2e8477f2e7cd', summaryConcept: { primaryConcept: '166448AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', }, }, { id: 'populationType', title: 'populationTypeTitle', encounterType: 'e22e39fd-7db2-45e7-80f1-60fa0d5a4378', concept: '164431AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', hasSummary: true, summaryConcept: { primaryConcept: '166433AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', secondaryConcept: '2bf14240-b2b2-42b2-8cf3-b5f8a0cb7764', }, }, ], }, { tileHeader: 'hivMonitoring', columns: [ { id: 'viralLoad', title: 'currentViralLoad', encounterType: '3596fafb-6f6f-4396-8c87-6e63a0f1bd71', concept: '1305AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', hasSummary: true, summaryConcept: { primaryConcept: '163724AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', isDate: true, }, }, { id: 'lastCD4Count', title: 'lastCD4CountTitle', hasSummary: true, encounterType: '3596fafb-6f6f-4396-8c87-6e63a0f1bd71', concept: '5497AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', summaryConcept: { primaryConcept: '163724AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', isDate: true, }, }, ], }, { tileHeader: 'lastVisitDetails', columns: [ { id: 'nextAppointmentDate', title: 'nextAppointmentDateTitle', hasSummary: true, encounterType: 'cb0a65a7-0587-477e-89b9-cf2fd144f1d4', concept: '5096AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', summaryConcept: { primaryConcept: '5096AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', isDate: true, hasCalculatedDate: true, }, }, { id: 'programStatus', title: 'programStatusTitle', encounterType: 'a221448d-512b-4750-84bf-d29be9f802b3', concept: '163105AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', }, ], }, ], [], ); const tilesData = useMemo( () => tilesDefinitions?.map((tile: any) => ({ title: tile.tileHeader, columns: getEncounterTileColumns(tile, t), })), [], ); return ( <> {tilesData?.length > 0 && tilesData?.map((tile, index) => ( <MemoizedEncounterTile key={index} patientUuid={patientUuid} columns={tile.columns} headerTitle={tile.title} /> ))} </> ); };
子组件代码
const EncounterTile: React.FC<EncounterTileProps> = ({ patientUuid, columns, headerTitle }) => { return ( <div className={styles.tileContainer}> <Tile className={styles.tile}> <div className={styles.cardTitle}> <h4 className={styles.title}> {headerTitle} </h4> </div> <Column className={styles.columnContainer}> {columns.map((column, ind) => ( <EncounterValuesTile key={ind} patientUuid={patientUuid} column={column} /> ))} </Column> </Tile> </div> ); }; export const MemoizedEncounterTile = React.memo(EncounterTile); export const EncounterValuesTile: React.FC<EncounterValuesTileProps> = ({ patientUuid, column }) => { const { lastEncounter, isLoading, error, isValidating } = useLastEncounter(patientUuid, column.encounterUuid); if (isLoading || isValidating) { return <CodeSnippetSkeleton type="multi" data-testid="skeleton-text" />; } if (error || lastEncounter === undefined) { return ( <div className={styles.tileBox}> <div className={styles.tileBoxColumn}> <span className={styles.tileTitle}> {column.header} </span> <span className={styles.tileValue}>--</span> </div> </div> ); } return ( <div className={styles.tileBox}> <div className={styles.tileBoxColumn}> <span className={styles.tileTitle}> {column.header} </span> <span className={styles.tileValue}> <LazyCell lazyValue={column.getObsValue(lastEncounter)} /> </span> {column.hasSummary && ( <span className={styles.tileTitle}> <LazyCell lazyValue={column.getSummaryObsValue(lastEncounter)} /> </span> )} </div> </div> ); };
咨询问题
- 如何确保请求串行执行,仅在前一个请求完成后才发起新请求?
- 有没有办法优化hook,避免对同一encounter type发起重复请求?
- 在React中使用useSWR这类hook时,处理此类场景有哪些最佳实践?
解决方案
问题1:实现请求串行执行
可以在父组件中集中管理请求顺序,先收集所有唯一的encounterType,再按顺序发起请求,将结果缓存后传递给子组件:
// 父组件中收集唯一的encounterType const uniqueEncounterTypes = useMemo(() => { const typeSet = new Set(); tilesDefinitions.forEach(tile => { tile.columns.forEach(col => typeSet.add(col.encounterType)); }); return Array.from(typeSet); }, [tilesDefinitions]); // 跟踪当前请求索引和缓存结果 const [currentIndex, setCurrentIndex] = useState(0); const [encounterCache, setEncounterCache] = useState<Record<string, any>>({}); useEffect(() => { if (currentIndex >= uniqueEncounterTypes.length) return; const targetType = uniqueEncounterTypes[currentIndex]; // 调用你的请求函数(复用useLastEncounter内部的fetch逻辑) fetchLastEncounter(patientUuid, targetType) .then(data => { setEncounterCache(prev => ({ ...prev, [targetType]: data })); setCurrentIndex(prev => prev + 1); }) .catch(() => { // 错误处理后继续下一个请求 setCurrentIndex(prev => prev + 1); }); }, [currentIndex, patientUuid, uniqueEncounterTypes]); // 将缓存结果传递给子组件 return ( <> {tilesData?.length > 0 && tilesData?.map((tile, index) => ( <MemoizedEncounterTile key={index} patientUuid={patientUuid} columns={tile.columns} headerTitle={tile.title} encounterCache={encounterCache} /> ))} </> );
子组件直接取用缓存数据,无需自己发起请求,从根源避免并行请求的竞态问题。
问题2:避免同一encounterType重复请求
SWR自带缓存去重机制,核心是确保请求key的唯一性:
- 修正useLastEncounter的key生成:
function useLastEncounter(patientUuid: string, encounterType: string) { // 用patientUuid+encounterType生成唯一key const swrKey = patientUuid && encounterType ? ['last-encounter', patientUuid, encounterType] : null; return useSWR(swrKey, () => fetchLastEncounter(patientUuid, encounterType), { dedupingInterval: 5000, // 5秒内相同请求自动合并 revalidateOnFocus: false, // 取消焦点重新验证,减少不必要请求 }); }
- Memo化子组件:用
React.memo包裹EncounterValuesTile,避免不必要的重渲染触发hook调用:
export const EncounterValuesTile = React.memo(({ patientUuid, column }) => { // 内部逻辑不变 });
- 检查参数传递:确认子组件接收的
encounterType没有拼写错误(你代码中写的column.encounterUuid可能是笔误,应为column.encounterType)。
问题3:React中使用useSWR的最佳实践
- 请求上移,集中管理:尽量在页面或父组件级别发起所有请求,将数据通过props传递给子组件,避免底层组件零散发起请求,便于控制请求顺序和缓存。
- 利用SWR缓存策略:合理配置
dedupingInterval、revalidateOnMount等参数,复用缓存减少重复请求;用mutate更新缓存,保证数据一致性。 - 组件优化:用
React.memo、useMemo、useCallback减少不必要的重渲染,避免hook重复执行。 - 统一状态处理:在父组件或封装的通用组件中统一处理加载、错误状态,避免每个子组件重复写相同逻辑。
- 循环中hook的注意事项:如果必须在循环中使用hook,确保每个hook的key唯一且稳定;优先在父组件中预处理参数,减少循环内的动态计算。
- 避免竞态:对于依赖顺序的请求,要么用父组件串行请求,要么用SWR的
suspense模式配合React Suspense统一处理加载状态。
内容的提问来源于stack exchange,提问作者shiro
相关产品推荐
相关产品推荐

