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
相关产品推荐
相关产品推荐

