React Hooks循环依赖触发Maximum call stack size exceeded报错求解
问题描述
将基于MobX开发的React Native应用迁移为ReactJS应用,采用React Hooks + Context + localStorage实现状态管理,运行时抛出RangeError: Maximum call stack size exceeded报错。
核心调用链形成循环依赖:
MainStore初始化时调用assets()获取接口方法assets()内部调用interceptedAxios()创建自定义axios实例interceptedAxios()初始化时反向调用MainStore()获取checkForRefreshTokenUpdate方法,触发无限递归调用
核心代码逻辑如下:
mainStore 核心代码
export const MainStore = () => { const AppContext = createContext(); const initialState = { ... } const reducer = (state, action) => { return { ...state, ...action }; } const [ initState, setInitState ] = useLocalStorage('state', initialState); const [ state, dispatch ] = useReducer(reducer, initState); const { getAssetData } = assets(); const getInstMetaData = async () => { const params = { asset_type_name: "...", with: "..." }; return getAssetData(params); }; const checkForRefreshTokenUpdate = async () => { ... } return { AppContext, initialState, state, dispatch, reducer, checkForRefreshTokenUpdate, getInstMetaData }; }
assets 核心代码
import { interceptedAxios } from "./axios/config"; import { buildUrl } from "./utils/urls"; import { getAccessToken, getBaseUrl } from "./index"; const assets = () => { const { customAxios } = interceptedAxios(); /** * Fetches asset data. * @returns */ const getAssetData = async (params) => { const url = buildUrl({ baseUrl: getBaseUrl(), endpoint: "/api/asset", params }); const reqOpts = { headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` } }; return customAxios.get(url, reqOpts).then(res => { res.data }); }; return { getAssetData } } export { assets };
interceptedAxios 核心代码
import axios from "axios"; import { MainStore } from "../../stores/mainStore"; import { getBaseUrl } from "../index"; export const interceptedAxios = () => { // Create axios instance for customization and import this instead of "axios" const customAxios = axios.create(); const { checkForRefreshTokenUpdate } = MainStore(); customAxios.interceptors.request.use(async (config) => { if (config.url.includes(getBaseUrl()) && config.headers["Authorization"] != null) { await checkForRefreshTokenUpdate() .then((token) => { if (token != null) { config.headers["Authorization"] = `Bearer ${token}`; } }); } return config; }, (error) => { return Promise.reject(error); }); return { customAxios } }
解决思路
方案1:抽离鉴权逻辑为独立模块(优先推荐)
循环依赖的核心原因是token刷新逻辑和全局Store、请求实例强耦合。将checkForRefreshTokenUpdate、token存取相关逻辑从MainStore中剥离,放到独立的公共工具模块(比如src/utils/auth.js),模块内不依赖Store、不依赖axios实例,只处理纯鉴权逻辑。
调整后:
- axios拦截器直接从auth工具模块引入
checkForRefreshTokenUpdate,不再依赖MainStore - MainStore需要使用鉴权相关方法时,同样从auth模块引入
完全打断循环依赖链,符合单一职责原则,后续维护成本最低。
另外注意现有代码存在一个隐性bug:getAssetData的then回调中res => { res.data }没有返回值,接口调用后会拿到undefined,需要改成res => res.data或手动添加return。
方案2:延迟注入拦截器依赖(无需拆分原有逻辑)
如果暂时不想重构现有代码结构,可以将拦截器的依赖注入时机延后,避开模块加载阶段的循环调用。
首先调整interceptedAxios文件:
import axios from "axios"; import { getBaseUrl } from "../index"; // 提前创建单例axios实例 const customAxios = axios.create(); let refreshTokenHandler = null; // 暴露依赖注入方法,待Store初始化完成后再传入需要的方法 export const setupAxiosInterceptors = (checkForRefreshTokenUpdate) => { refreshTokenHandler = checkForRefreshTokenUpdate; customAxios.interceptors.request.use(async (config) => { if (config.url.includes(getBaseUrl()) && config.headers["Authorization"] != null) { const token = await refreshTokenHandler(); if (token != null) { config.headers["Authorization"] = `Bearer ${token}`; } } return config; }, (error) => Promise.reject(error)); } export const interceptedAxios = () => { return { customAxios } }
之后在应用根组件初始化Store完成后,注入依赖:
import { setupAxiosInterceptors } from "./axios/config"; import { MainStore } from "./stores/mainStore"; function App() { const store = MainStore(); // Store初始化完成后再注入拦截器依赖 useEffect(() => { setupAxiosInterceptors(store.checkForRefreshTokenUpdate); }, []) return ( <store.AppContext.Provider value={store}> {/* 应用路由与业务组件 */} </store.AppContext.Provider> ) }
该方案不需要拆分原有业务逻辑,仅调整依赖获取时机,改造成本极低。
方案3:通过Context透传请求依赖(不推荐)
保留原有代码结构的前提下,可以将请求方法封装为自定义Hook,在Hook内部通过useContext获取Store中的刷新方法。但该方案要求所有接口调用必须在React函数组件或自定义Hook内执行,灵活性差,业务侵入性高,不推荐使用。
内容的提问来源于stack exchange,提问作者billysk
相关产品推荐
相关产品推荐

