React 数据路由器 - 将上下文传递给加载器

Mis*_*a M 11 javascript reactjs react-router react-router-dom

我有一个基于 JWT 的 API。它会在每次响应时轮换令牌。我有一个自定义提供者来管理这个。

我试图弄清楚如何通过此设置使用 React Router v6.4 数据路由器。具体来说,我想使用loader/action函数来获取数据,但这些函数不支持useContext,我不知道如何传递它。

我想dashboardLoader使用当前的令牌集作为为我管理的标头来调用 API AuthContext

目标是让该loader函数获取一些数据以显示在仪表板上并使用get()来自AuthProvider.

我当前的选择是在组件内部执行此操作Dashboard,但想了解如何使用加载程序执行此操作。

相关文件:

// App.js
import "./App.css";

import { createBrowserRouter } from "react-router-dom";

import "./App.css";

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,
  },
]);

Run Code Online (Sandbox Code Playgroud)
// AuthProvider
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>
  );
}
Run Code Online (Sandbox Code Playgroud)
// Dashboard
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>;
}
Run Code Online (Sandbox Code Playgroud)

Dre*_*ese 3

按原样创建 axios 实例,但您将调整AuthProvider以添加请求和响应拦截器来处理令牌和标头。apiClient您还将传递对加载器函数的引用dashboardLoader

认证提供者

将访问令牌存储在 React ref 中,并直接使用/引用传递的内容,apiClient而不是将其存储在本地组件状态中(React 反模式)。添加一个useEffect钩子来添加请求和响应拦截器来维护该accessTokenRef值。

export function AuthProvider({ apiClient }) {
  const accessTokenRef = useRef();

  useEffect(() => {
    const requestInterceptor = apiClient.interceptors.request.use(
      (config) => {
        // Attach current access token ref value to outgoing request headers
        config.headers["access-token"] = accessTokenRef.current;
        return config;
      },
    );

    const responseInterceptor = apiClient.interceptors.response.use(
      (response) => {
        // Cache new token from incoming response headers
        accessTokenRef.current = response.headers["access-token"];
        return response;
      },
    );

    // Return cleanup function to remove interceptors if apiClient updates
    return () => {
      apiClient.interceptors.request.eject(requestInterceptor);
      apiClient.interceptors.response.eject(responseInterceptor);
    };
  }, [apiClient]);

  async function login(email, password, callback) {
    try {
      const reqData = { email, password };
      await apiClient.post("/auth/sign_in", reqData);

      callback();
    } catch (e) {
      console.error(e);
      throw e;
    }
  }

  function isLoggedIn() {
    return accessTokenRef.current === "";
  }

  const context = {
    login,
    isLoggedIn,
    get: apiClient.get,
    post: apiClient.post,
  };

  return (
    <AuthContext.Provider value={context}>
      {props.children}
    </AuthContext.Provider>
  );
}
Run Code Online (Sandbox Code Playgroud)

仪表板

请注意,这loader是一个柯里化函数,例如,一个使用单个参数并返回另一个函数的函数。这是为了在回调作用域中消耗并关闭apiClient.

export const loader = (apiClient) => ({ params, request }) {
  // Use passed apiClient to call the API
  // For example:
  // const response = await apiClient.get("/my-data");
  // return response.data;
}
Run Code Online (Sandbox Code Playgroud)

应用程序.js

import { createBrowserRouter } from "react-router-dom";
import axios from "axios";
import "./App.css";
import Dashboard, { loader as dashboardLoader } from "./dashboard";
import AuthProvider from "./AuthProvider";

function newApiClient() {
  return axios.create({
    baseURL: "http://localhost:3000",
    headers: {
      "Content-Type": "application/json",
    },
  });
}

const apiClient = newApiClient();

export const router = createBrowserRouter([
  {
    path: "/",
    element: (<h1>Welcome</h1>),
  },
  {
    path: "/dashboard",
    element: (
      <AuthProvider apiClient={apiClient}> // <-- pass apiClient
        <Dashboard />
      </AuthProvider>
    ),
    loader: dashboardLoader(apiClient), // <-- pass apiClient
  },
]);
Run Code Online (Sandbox Code Playgroud)