React Router v6.4数据路由:如何向loader传递AuthContext?
问题描述
我有一个基于JWT的API,每次响应都会刷新令牌,已经实现了自定义Provider来管理该令牌。
现在想结合这个配置使用React Router v6.4的data router,希望用loader/action函数获取数据,但这些函数不支持useContext,不知道怎么传递AuthContext。
我希望dashboardLoader能使用AuthContext管理的当前令牌作为请求头调用API,让loader获取仪表盘数据并调用AuthProvider的get()方法。目前我是在Dashboard组件内实现数据获取的,想了解用loader的实现方式。
相关代码
App.js
import "./App.css"; import { createBrowserRouter } from "react-router-dom"; import Dashboard, { loader as dashboardLoader } from "./dashboard"; import AuthProvider from "./AuthProvider"; import axios from "axios"; function newApiClient() { return axios.create({ baseURL: "http://localhost:3000", headers: { "Content-Type": "application/json", }, }); } const api = newApiClient(); export const router = createBrowserRouter([ { path: "/", element: (<h1>Welcome</h1>), }, { path: "/dashboard", element: ( <AuthProvider apiClient={api}> <Dashboard /> </AuthProvider> ), loader: dashboardLoader, }, ]);
AuthProvider.js
import { createContext, useState } from "react"; const AuthContext = createContext({ login: (email, password) => {}, isLoggedIn: () => {}, get: async () => {}, post: async () => {}, }); export function AuthProvider(props) { const [authData, setAuthData] = useState({ client: props.apiClient, accessToken: "", }); async function login(email, password, callback) { try { const reqData = { email: email, password: password }; await post("/auth/sign_in", reqData); callback(); } catch (e) { console.error(e); throw e; } } function isLoggedIn() { return authData.accessToken === ""; } async function updateTokens(headers) { setAuthData((prev) => { return { ...prev, accessToken: headers["access-token"], }; }); } async function get(path) { try { const response = await authData.client.get(path, { headers: { "access-token": authData.accessToken }, }); await updateTokens(response.headers); return response; } catch (error) { console.error(error); throw error; } } async function post(path, data) { try { const response = await authData.client.post(path, data, { headers: { "access-token": authData.accessToken }, }); await updateTokens(response.headers); return response.data; } catch (error) { // TODO console.error(error); throw error; } } const context = { login: login, isLoggedIn: isLoggedIn, get: get, post: post, }; return ( <AuthContext.Provider value={context}> {props.children} </AuthContext.Provider> ); } export default AuthContext;
Dashboard.js
import { useContext } from "react"; import { Navigate } from "react-router-dom"; import AuthContext from "./AuthProvider"; export function loader() { // TODO use auth context to call the API // For example: // const response = await auth.get("/my-data"); // return response.data; } export default function Dashboard() { const auth = useContext(AuthContext); if (!auth.isLoggedIn()) { return <Navigate to="/" replace />; } return <h1>Dashboard Stuff</h1>; }
解决方案
React Router的loader/action是在组件渲染流程外执行的,确实无法直接调用useContext。以下是两种可行的实现方案:
方案1:利用axios拦截器全局管理令牌
把令牌逻辑绑定到axios实例上,让请求自动携带令牌、响应自动更新令牌,这样loader可以直接调用axios实例,无需依赖Context。
步骤1:改造axios实例,添加拦截器
// App.js function newApiClient() { const client = axios.create({ baseURL: "http://localhost:3000", headers: { "Content-Type": "application/json", }, }); // 全局存储当前令牌,也可以用localStorage持久化 let currentAccessToken = ""; // 请求拦截器:自动在请求头添加令牌 client.interceptors.request.use(config => { if (currentAccessToken) { config.headers["access-token"] = currentAccessToken; } return config; }); // 响应拦截器:更新令牌 client.interceptors.response.use(response => { const newToken = response.headers["access-token"]; if (newToken) { currentAccessToken = newToken; } return response; }, error => { // 可在此处理令牌过期、权限错误等逻辑 return Promise.reject(error); }); // 暴露设置令牌的方法,供AuthProvider调用 client.setAccessToken = (token) => { currentAccessToken = token; }; return client; }
步骤2:修改AuthProvider同步令牌到axios实例
// AuthProvider.js import { createContext, useState, useEffect } from "react"; export function AuthProvider(props) { const [accessToken, setAccessToken] = useState(""); const apiClient = props.apiClient; // 同步令牌到axios实例 useEffect(() => { apiClient.setAccessToken(accessToken); }, [accessToken, apiClient]); async function updateTokens(headers) { const newToken = headers["access-token"]; if (newToken) { setAccessToken(newToken); } } // 简化get/post方法,不再手动添加令牌 async function get(path) { try { const response = await apiClient.get(path); await updateTokens(response.headers); return response; } catch (error) { console.error(error); throw error; } } async function post(path, data) { try { const response = await apiClient.post(path, data); await updateTokens(response.headers); return response.data; } catch (error) { console.error(error); throw error; } } // ... 其余代码不变 }
步骤3:在loader中直接调用axios实例
// Dashboard.js import { redirect } from "react-router-dom"; import api from "./App"; // 导入全局axios实例 export async function loader() { try { const response = await api.get("/my-data"); return response.data; } catch (error) { // 无权限或请求失败时重定向到首页 throw redirect("/"); } }
方案2:通过Router Context注入Auth管理对象
利用React Router的createBrowserRouter提供的context配置,把Auth管理逻辑注入到全局路由上下文,让所有loader/action都能访问。
步骤1:创建全局Auth管理类
// App.js class AuthManager { constructor(apiClient) { this.apiClient = apiClient; this.accessToken = ""; this.tokenUpdateListeners = []; } async login(email, password) { try { const response = await this.apiClient.post("/auth/sign_in", { email, password }); this.setAccessToken(response.headers["access-token"]); return response.data; } catch (error) { throw error; } } isLoggedIn() { return !!this.accessToken; } setAccessToken(token) { this.accessToken = token; // 通知所有监听者令牌更新 this.tokenUpdateListeners.forEach(listener => listener(token)); } // 添加令牌更新监听,供AuthProvider同步组件状态 onTokenUpdate(listener) { this.tokenUpdateListeners.push(listener); return () => { this.tokenUpdateListeners = this.tokenUpdateListeners.filter(l => l !== listener); }; } async get(path) { try { const response = await this.apiClient.get(path, { headers: { "access-token": this.accessToken } }); this.setAccessToken(response.headers["access-token"]); return response; } catch (error) { throw error; } } } const api = newApiClient(); export const authManager = new AuthManager(api); // 创建router时注入context export const router = createBrowserRouter([ { path: "/", element: (<h1>Welcome</h1>), }, { path: "/dashboard", element: ( <AuthProvider authManager={authManager}> <Dashboard /> </AuthProvider> ), loader: dashboardLoader, }, ], { context: { authManager } });
步骤2:修改AuthProvider绑定全局AuthManager
// AuthProvider.js export function AuthProvider(props) { const [accessToken, setAccessToken] = useState(props.authManager.accessToken); // 监听令牌更新,同步组件状态 useEffect(() => { return props.authManager.onTokenUpdate(setAccessToken); }, [props.authManager]); const context = { login: props.authManager.login.bind(props.authManager), isLoggedIn: props.authManager.isLoggedIn.bind(props.authManager), get: props.authManager.get.bind(props.authManager), post: props.authManager.post?.bind(props.authManager), // 按需实现post方法 }; return ( <AuthContext.Provider value={context}> {props.children} </AuthContext.Provider> ); }
步骤3:在loader中通过路由上下文访问AuthManager
// Dashboard.js import { redirect } from "react-router-dom"; export async function loader({ context }) { if (!context.authManager.isLoggedIn()) { throw redirect("/"); } try { const response = await context.authManager.get("/my-data"); return response.data; } catch (error) { throw redirect("/"); } }
方案对比
- 方案1适合简单场景,无需额外状态库,利用axios拦截器自动处理令牌,代码简洁。
- 方案2更灵活,适合复杂权限逻辑,路由上下文可在所有loader/action中访问,状态同步更可控。
内容的提问来源于stack exchange,提问作者Misha M
相关产品推荐
相关产品推荐

