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

React Context中使用Suspense加载JSON数据触发无限循环报错

核心问题排查

你的代码有4个核心bug导致死循环、Suspense不生效:

  • readFile函数逻辑错误:XMLHttpRequest是异步API,你在调用send()后立刻判断请求状态,此时请求还未完成,永远拿不到正确响应,也不会触发正确resolve
  • 违反React渲染纯函数原则:在子组件渲染阶段调用getData时直接修改模块级全局变量data,既没有通过setState通知React更新,还会导致渲染过程中出现不可预期的变量修改,触发控制台警告和重渲染死循环
  • Suspense触发逻辑缺失:Suspense能生效的核心是组件渲染阶段抛出未完成的Promise,你的代码仅在数据已经是Promise状态时才throw,第一次发起请求时只是把Promise存在data里就直接返回,Suspense完全捕获不到加载状态,自然不会显示fallback
  • 全局缓存设计缺陷:模块级的data变量脱离React状态管理,Promise resolve后直接修改data属性不会触发组件重渲染,就算数据加载完成页面也不会更新
修复方案

1. 修复JSON请求逻辑

给XMLHttpRequest绑定状态监听事件,不要在send后立刻判断结果:

// jsonLoader.js
export const loadJSON = async (path) => {
  const json = await readFile(path, 'application/json');
  return json ? JSON.parse(json) : undefined;
};

const readFile = async (path, mimeType) =>
  new Promise((resolve) => {
    const xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = () => {
      if (xmlHttp.readyState === 4) {
        resolve(xmlHttp.status === 200 ? xmlHttp.responseText : undefined);
      }
    };
    xmlHttp.onerror = () => resolve(undefined);
    xmlHttp.open('GET', path, true);
    if (mimeType && xmlHttp.overrideMimeType) xmlHttp.overrideMimeType(mimeType);
    xmlHttp.send();
  });

2. 重构DataContext逻辑

将数据纳入React状态管理,正确实现Suspense的Promise抛出逻辑,独立维护请求缓存:

// DataContext.js
import { createContext, useContext, useState, useMemo, Suspense } from 'react';
import { loadJSON } from './jsonLoader';
import { UserContext } from './UserContext';

// 独立请求缓存,避免重复发起相同请求
// 缓存结构:{ [请求路径]: { status: 'pending'|'success'|'error', value?: any, promise?: Promise } }
const requestCache = new Map();

// 工具函数:按路径读取对象属性
const getValueByPath = (obj, path) => path.split('.').reduce((o, i) => o?.[i], obj);

const DataContextInstance = createContext({
  getData: () => ({}),
  getTranslations: () => ({}),
});

export const DataContext = ({ dataLoaded, children }) => {
  const { lang } = useContext(UserContext);
  // 所有已加载数据存入state,修改时自动触发重渲染
  const [loadedData, setLoadedData] = useState(() => dataLoaded || {});

  const loadData = (dataPath) => {
    const filePath = `/data/${dataPath || `translations/${lang}`}.json`;
    // 命中缓存直接处理
    if (requestCache.has(filePath)) {
      const cachedItem = requestCache.get(filePath);
      if (cachedItem.status === 'success') return cachedItem.value;
      if (cachedItem.status === 'pending') throw cachedItem.promise;
      throw cachedItem.value;
    }
    // 未命中缓存,发起新请求
    const requestPromise = loadJSON(filePath)
      .then((res) => {
        requestCache.set(filePath, { status: 'success', value: res });
        setLoadedData(prev => ({ ...prev, [dataPath]: res }));
        return res;
      })
      .catch(err => {
        requestCache.set(filePath, { status: 'error', value: err });
        throw err;
      });
    requestCache.set(filePath, { status: 'pending', promise: requestPromise });
    // 抛出Promise触发最近的Suspense边界显示fallback
    throw requestPromise;
  };

  const getData = (...paths) => {
    // 所有要求的路径数据都已加载时直接返回数据
    const isAllLoaded = paths.every(p => getValueByPath(loadedData, p) !== undefined);
    if (isAllLoaded) return loadedData;
    // 存在未加载数据,逐个触发加载
    paths.forEach(p => loadData(p));
    return loadedData;
  };

  const contextValue = useMemo(
    () => ({
      getData,
      getTranslations: () => getData()
    }),
    [loadedData, lang]
  );

  return (
    <DataContextInstance.Provider value={contextValue}>
      <Suspense fallback={<span>Loading...</span>}>
        {children}
      </Suspense>
    </DataContextInstance.Provider>
  );
};

export const useDataContext = () => {
  const context = useContext(DataContextInstance);
  if (!context) throw new Error('Context must be used within a Provider');
  return context;
};

3. 其他调整说明

  • 原有未使用的setter函数可以直接删除,数据更新统一通过setLoadedData执行,保证React能感知状态变化
  • 业务组件(比如NavBar)中的调用逻辑不需要修改,正常调用getData获取需要的数据即可
  • 如果需要更细粒度的加载状态,可以把Suspense边界下沉到具体业务组件层级,不用全部放在DataContext根节点
  • 切换语言时,如果需要重新拉取对应语言的翻译文件,可以在lang变化时清空requestCache里对应翻译路径的缓存

内容的提问来源于stack exchange,提问作者Marc J Cabrer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 10:18:06