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

如何让React Router Loader等待Axios实例初始化完成?

解决React Router v6 Loader提前执行导致Axios未配置授权头的401问题

问题背景

我封装了Axios实例组件AxiosSettings,用来设置授权请求头和401刷新令牌的拦截器,依赖useAuth Hooks获取用户信息。但使用React Router v6的数据路由时,刷新页面(比如/profile路由),loader函数会在所有useEffect之前执行,导致Axios还没设置好Authorization头就发起请求,直接返回401错误——哪怕AxiosSettings渲染时useAuth已经拿到了用户信息。

相关代码:

AxiosSettings.tsx

import axios from 'axios';
import useAuth from '../hooks/useAuth';
import { useEffect, ReactNode } from 'react';

const api = axios.create({
    baseURL: 'http://127.0.0.1:8000/api/',
    headers: {
        "Content-Type": "application/json",
    },
})

export function AxiosSettings({children}: { children: ReactNode }) {
    const {user, login} = useAuth()

    useEffect(() => {
        console.log('effect 1')
        if (user) {
            api.defaults.headers.common["Authorization"] = `Bearer ${user.accessToken}`
        } else {
            delete api.defaults.headers.common["Authorization"]
        }
    }, [user])

    useEffect(() => {
        console.log('effect 2')
        const interceptor = api.interceptors.response.use(
            (response) => response,
            async (error) => {
                if (error.response?.status === 401) {
                    // 这里是刷新JWT的逻辑
                }
                return Promise.reject(error);
            }
        )
        return () => api.interceptors.response.eject(interceptor)
    }, []);

    return children;
}

export default api

App.tsx

import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom';
import { useState } from 'react';
import { AuthContext } from './contexts/AuthContext';
import { AxiosSettings } from './AxiosSettings';
// 导入其他组件...

const router = createBrowserRouter(createRoutesFromElements(
    <Route element={<RootLayout/>}>
        <Route index element={<Home/>}/>
        <Route path="register" element={<Signup/>}/>
        <Route path="login" element={<Login/>}/>
        <Route path="about"/>

        <Route element={<RequireAuth/>}>
            <Route path="profile" element={<ProfileLayout/>} loader={ProfileLoader}>
                <Route index element={<ProfileIndex/>} loader={PrizesLoader}/>
                <Route path="history"/>
                <Route path="invitations"/>
            </Route>
        </Route>

        <Route path="admin" element={<RequireAdmin/>}>
            <Route index element={<AdminPage/>}/>
            <Route path="game/:hash" element={<Game/>}/>
        </Route>
    </Route>
))

function App() {
    const [user, setUser] = useState<User | null>(() => {
        return SessionStorageUserService.get();
    })

    return (
        <AuthContext.Provider value={{user, setUser}}>
            <AxiosSettings>
                <RouterProvider router={router} />
            </AxiosSettings>
        </AuthContext.Provider>
    )
}

解决方案

方案1:将Axios配置逻辑移出组件,同步依赖SessionStorage

既然user是从SessionStorage初始化的,不需要等组件渲染,直接在Axios实例创建后就同步设置授权头,拦截器也直接挂载,不用放到useEffect里。

修改AxiosSettings.tsx(或单独抽离api配置文件):

import axios from 'axios';
import SessionStorageUserService from '../services/SessionStorageUserService';
import { User } from '../types';
import { ReactNode, useEffect } from 'react';
import useAuth from '../hooks/useAuth';

const api = axios.create({
    baseURL: 'http://127.0.0.1:8000/api/',
    headers: {
        "Content-Type": "application/json",
    },
})

// 初始化时直接从SessionStorage拿用户信息设置头
const initUser = SessionStorageUserService.get();
if (initUser) {
    api.defaults.headers.common["Authorization"] = `Bearer ${initUser.accessToken}`;
}

// 直接挂载拦截器
api.interceptors.response.use(
    (response) => response,
    async (error) => {
        if (error.response?.status === 401) {
            // 刷新JWT逻辑
            const newUser = await refreshToken();
            if (newUser) {
                SessionStorageUserService.set(newUser);
                api.defaults.headers.common["Authorization"] = `Bearer ${newUser.accessToken}`;
                // 重试原请求
                return api(error.config);
            }
            // 刷新失败跳转登录页
            window.location.href = '/login';
        }
        return Promise.reject(error);
    }
)

// 提供方法供Auth状态变化时同步更新Axios头
export function updateAxiosAuthHeader(user: User | null) {
    if (user) {
        api.defaults.headers.common["Authorization"] = `Bearer ${user.accessToken}`;
    } else {
        delete api.defaults.headers.common["Authorization"];
    }
}

export function AxiosSettings({children}: { children: ReactNode }) {
    const {user} = useAuth();

    // 用户状态变化时同步更新Axios头
    useEffect(() => {
        updateAxiosAuthHeader(user);
    }, [user]);

    return children;
}

export default api

然后在useAuth Hook中同步调用更新方法:

// useAuth.ts
import { useContext } from 'react';
import { AuthContext } from '../contexts/AuthContext';
import { updateAxiosAuthHeader } from '../AxiosSettings';
import { User } from '../types';

export function useAuth() {
    const {user, setUser} = useContext(AuthContext);

    const login = (newUser: User) => {
        setUser(newUser);
        updateAxiosAuthHeader(newUser);
    };

    const logout = () => {
        setUser(null);
        updateAxiosAuthHeader(null);
    };

    return {user, login, logout};
}

这种方式下,刷新页面时Axios初始化就已经拿到用户信息并配置好授权头,loader执行时请求会携带正确的头,不会触发401。

方案2:延迟RouterProvider渲染,直到配置完成

如果必须在组件内完成Axios配置,可以在App组件中等待用户初始化完成后再渲染RouterProvider:

function App() {
    const [user, setUser] = useState<User | null>(null);
    const [isReady, setIsReady] = useState(false);

    useEffect(() => {
        const initUser = SessionStorageUserService.get();
        setUser(initUser);
        // 同步设置Axios头
        if (initUser) {
            api.defaults.headers.common["Authorization"] = `Bearer ${initUser.accessToken}`;
        }
        setIsReady(true);
    }, []);

    if (!isReady) {
        return <div>加载中...</div>; // 可替换为骨架屏提升体验
    }

    return (
        <AuthContext.Provider value={{user, setUser}}>
            <AxiosSettings>
                <RouterProvider router={router} />
            </AxiosSettings>
        </AuthContext.Provider>
    )
}

这种方式会先完成用户初始化和Axios配置,再渲染路由,确保loader在配置完成后执行,但会出现短暂的加载状态。

方案3:在loader中手动设置授权头

不想修改全局Axios配置的话,可以在每个需要授权的loader里手动添加Authorization头,或者封装授权请求函数:

// 封装授权请求函数
async function authRequest(url: string, config = {}) {
    const user = SessionStorageUserService.get();
    if (!user) {
        throw redirect('/login');
    }
    return api(url, {
        ...config,
        headers: {
            ...config.headers,
            'Authorization': `Bearer ${user.accessToken}`
        }
    });
}

// 在loader中调用
async function ProfileLoader() {
    const data = await authRequest('/profile');
    return data;
}

这种方式不依赖组件的useEffect,直接在loader中获取用户信息并设置头,避开了loader提前执行的问题。

总结

推荐方案1,将Axios核心配置与组件解耦,同步依赖SessionStorage初始化,同时提供状态同步方法,既解决了刷新时的401问题,也保证了用户状态变化时Axios配置的一致性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 22:28:14