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

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>
  );
};

咨询问题

  1. 如何确保请求串行执行,仅在前一个请求完成后才发起新请求?
  2. 有没有办法优化hook,避免对同一encounter type发起重复请求?
  3. 在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的唯一性:

  1. 修正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, // 取消焦点重新验证,减少不必要请求
  });
}
  1. Memo化子组件:用React.memo包裹EncounterValuesTile,避免不必要的重渲染触发hook调用:
export const EncounterValuesTile = React.memo(({ patientUuid, column }) => {
  // 内部逻辑不变
});
  1. 检查参数传递:确认子组件接收的encounterType没有拼写错误(你代码中写的column.encounterUuid可能是笔误,应为column.encounterType)。

问题3:React中使用useSWR的最佳实践

  1. 请求上移,集中管理:尽量在页面或父组件级别发起所有请求,将数据通过props传递给子组件,避免底层组件零散发起请求,便于控制请求顺序和缓存。
  2. 利用SWR缓存策略:合理配置dedupingInterval、revalidateOnMount等参数,复用缓存减少重复请求;用mutate更新缓存,保证数据一致性。
  3. 组件优化:用React.memo、useMemo、useCallback减少不必要的重渲染,避免hook重复执行。
  4. 统一状态处理:在父组件或封装的通用组件中统一处理加载、错误状态,避免每个子组件重复写相同逻辑。
  5. 循环中hook的注意事项:如果必须在循环中使用hook,确保每个hook的key唯一且稳定;优先在父组件中预处理参数,减少循环内的动态计算。
  6. 避免竞态:对于依赖顺序的请求,要么用父组件串行请求,要么用SWR的suspense模式配合React Suspense统一处理加载状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 10:29:54