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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 14:30:16