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

React Hooks循环依赖触发Maximum call stack size exceeded报错求解

问题描述

将基于MobX开发的React Native应用迁移为ReactJS应用,采用React Hooks + Context + localStorage实现状态管理,运行时抛出RangeError: Maximum call stack size exceeded报错。
核心调用链形成循环依赖:

  1. MainStore初始化时调用assets()获取接口方法
  2. assets()内部调用interceptedAxios()创建自定义axios实例
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 21:09:34