如何让React Router Loader等待Axios实例初始化完成?
问题背景
我封装了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

