Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React data router - pass context to loader

I have a JWT-based API. It rotates the tokens on every response. I have a custom provider that manages this.

I'm trying to figure out how I would use React Router v6.4 data router with this setup. Specifically, I'd like to use the loader / action functions for getting the data, but those don't support useContext and I'm not sure how to pass that in.

I'd like dashboardLoader to call the API with the current set of tokens as headers that AuthContext is managing for me.

The goal is to have the loader function fetch some data to display on the dashboard and to use the get() call from the AuthProvider.

My current alternative is to just do it inside the Dashboard component but would like to see how to do this with a loader.

The relevant files:

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

// 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>
  );
}
// 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>;
}
like image 477
Misha M Avatar asked Apr 09 '26 11:04

Misha M


1 Answers

Create the axios instance as you are, but you'll tweak the AuthProvider to add request and response interceptors to handle the token and header. You'll pass a reference to the apiClient to the dashboardLoader loader function as well.

AuthProvider

Store the access token in a React ref and directly consume/reference the passed apiClient instead of storing it in local component state (a React anti-pattern). Add a useEffect hook to add the request and response interceptors to maintain the accessTokenRef value.

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

Dashboard

Note here that loader is a curried function, e.g. a function that consumes a single argument and returns another function. This is to consume and close over in callback scope the instance of the 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;
}

App.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
  },
]);
like image 55
Drew Reese Avatar answered Apr 12 '26 00:04

Drew Reese



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!