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

React中Axios拦截器加Token遇页面刷新/无限循环问题求助

问题:Axios拦截器配合React Auth上下文导致无限刷新/循环问题

我需要为HTTP请求添加Access Token,采用的方案是使用useEffect监听auth对象变化,以此配置Axios请求拦截器(同时移除旧拦截器,避免使用过期的token),实现代码如下:

import { InternalAxiosRequestConfig } from "axios";
import { AuthContextProps, useAuth } from "react-oidc-context";
import { AXIOS_INSTANCE } from "./api_mutator";
import { AppSettings, useAppSettings } from "./appSettings";
import { createContext, useContext, useEffect, useState } from "react";

export const ApiContext = createContext<null | string>(null);

export const ApiProvider = ({ children }) => {
    const appSettings = useAppSettings();
    const auth = useAuth();

    const [axiosInterceptorId, setAxiosInterceptorId] = useState<null | number>(null);

    useEffect(() => {
        console.log('ApiProvider.useEffect'); // 临时缓解刷新问题,但未解决根源

        // 移除旧拦截器
        if (axiosInterceptorId) {
            AXIOS_INSTANCE.interceptors.request.eject(axiosInterceptorId);
            setAxiosInterceptorId(null);
        }

        // 检查必要条件是否满足
        const hasValidSettings = !!appSettings?.apiBaseUrl && !!appSettings?.truthPortalApi;
        const hasValidAuth = auth?.isAuthenticated && !!auth.user?.access_token;

        if (hasValidSettings && hasValidAuth) {
            // 保存最新的auth和settings引用,避免闭包陷阱
            const latestAuth = auth;
            const latestAppSettings = appSettings;

            // 添加新拦截器
            const interceptorId = AXIOS_INSTANCE.interceptors.request.use((config) => {
                return {
                    ...config,
                    baseURL: latestAppSettings.truthPortalApi,
                    headers: {
                        ...config.headers,
                        Authorization: `Bearer ${latestAuth.user.access_token}`,
                    },
                };
            });
            setAxiosInterceptorId(interceptorId);
        }

        // 清理函数:组件卸载或依赖变化时移除拦截器
        return () => {
            if (axiosInterceptorId) {
                AXIOS_INSTANCE.interceptors.request.eject(axiosInterceptorId);
            }
        };
    }, [auth.isAuthenticated, auth.user?.access_token, appSettings?.apiBaseUrl, appSettings?.truthPortalApi]);

    // 直接从auth推导token,无需额外维护状态
    const accessToken = auth?.isAuthenticated && auth.user?.access_token ? auth.user.access_token : null;

    return (
        <ApiContext.Provider value={accessToken}>
            {children}
        </ApiContext.Provider>
    );
};

// Hook for consumers to know whether the API is available.
export function useApi() {
    const at = useContext(ApiContext);
    return at != null && at !== '';
}

遇到的问题

  • 上述代码会触发数百次页面刷新,添加console.log('ApiProvider.useEffect');后该现象暂时停止;
  • 有人告知无需使用useEffect,但未说明原因及替代方案,我认为必须用useEffect监听auth对象变化,否则无法获取更新后的token;
  • 若将拦截器相关逻辑移出useEffect,直接放在组件函数中,页面会变白并触发无限循环错误。

原因分析

  1. 无限刷新根源:之前监听的是整个auth和appSettings对象,而这两个对象可能在每次组件渲染时生成新引用(比如useAuth/useAppSettings内部状态更新时返回新对象),导致useEffect频繁触发,反复添加/移除拦截器,引发重渲染循环。添加console.log只是因控制台输出的性能开销轻微延迟了渲染,并未解决根本问题。
  2. 移出useEffect导致无限循环:组件函数每次渲染都会执行,若把拦截器逻辑直接放在这里,每次渲染都会添加新拦截器,而拦截器修改可能间接触发上下文/状态变化,导致组件再次渲染,形成无限循环。
  3. 关于是否需要useEffect:监听auth状态变化确实需要useEffect——拦截器配置属于副作用操作(修改全局Axios实例),必须放在useEffect中避免每次渲染都执行。说不需要的人可能没考虑副作用时机,或有特定场景的替代方案,但你的场景下useEffect是合理选择,只是依赖项配置有误。

解决方案

1. 优化useEffect依赖项

不要监听整个auth或appSettings对象,而是监听具体影响拦截器配置的字段(比如auth.isAuthenticated、auth.user?.access_token、appSettings?.apiBaseUrl等)。只有当这些关键值变化时,才触发拦截器更新,避免不必要的useEffect执行。

2. 修复闭包陷阱

原代码中拦截器函数引用的是useEffect触发时的auth/appSettings快照,后续auth更新后,拦截器里的token还是旧值。通过在useEffect内部保存最新引用,确保拦截器能获取当前最新的token和配置。

3. 移除不必要的状态

原代码中的access_token状态可直接从auth对象推导,无需额外维护,减少状态更新带来的重渲染触发。

4. 完善清理函数

在useEffect的返回函数中明确移除拦截器,确保组件卸载或依赖变化时,旧拦截器被正确清理,避免内存泄漏或残留旧拦截器干扰请求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 16:44:57