skip to Main Content

I created a custom hook and created an axios instance in it for fetching new tokens whenever the access token expires , initially on the page load the token seems to be fine , but after the timex expires my token is not getting set whats wrong in my code .

This is my useAxiosInstance.js file

import axios from "axios";
import { jwtDecode } from "jwt-decode";
import { useDispatch } from "react-redux";
import { authSuccess } from "../redux/auth/authSlice";
let userData = localStorage.getItem("userData")
  ? JSON.parse(localStorage.getItem("userData"))
  : null;
export const axiosInstance = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  headers: {
    Authorization: userData?.accessToken
      ? `Bearer ${userData.accessToken}`
      : "",
  },
});

const useAxiosInstance = () => {
  const getTokens = async (data) => {
    try {
      const res = await axios.post(
        `${process.env.REACT_APP_API_URL}refresh`,
        data,
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );
      return res.data;
    } catch (err) {
      return err;
    }
  };

  axiosInstance.interceptors.request.use(
    async (config) => {
      const accessTokenExpiryDate = jwtDecode(userData?.accessToken);

      console.log(accessTokenExpiryDate,"ex")

      if (accessTokenExpiryDate.exp * 1000 < new Date().getTime()) {
        const { accessToken, refreshToken } = await getTokens(
          JSON.stringify({ token: userData.refreshToken })
        );

        userData = {
          ...userData,
          accessToken,
          refreshToken,
        };
        localStorage.setItem("userData", JSON.stringify(userData));
        config.headers["Authorization"] = "Bearer " + accessToken;
      }
      return config;
    },
    (error) => Promise.reject(error)
  );

  return userData;
};

export default useAxiosInstance;

This is my app.js file

import { useDispatch, useSelector } from "react-redux";
import "./app.less";
import Header from "./components/header/Header";
import { BrowserRouter } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { useEffect } from "react";
import { authSuccess } from "./redux/auth/authSlice";
import AppRoute from "./routes/AppRoute";
import useAxiosInstance from "./utilities/useAxiosInstance"

function App() {
  const theme = useSelector((state) => state.theme);
  const auth = useSelector((state) => state.auth);
  const dispatch = useDispatch();

  const userData = useAxiosInstance();

  useEffect(() => {
    dispatch(authSuccess(userData));
  }, [userData, dispatch]);


  return (
    <BrowserRouter>
      <div
        className={theme.mode === "dark" ? "dark-mode app" : "light-mode app"}
      >
        {auth?.data && <Header />}
        <ToastContainer />
        <AppRoute />
      </div>
    </BrowserRouter>
  );
}

export default App;

This is my auth slice file

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  data: null,
};

const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    authSuccess: (state, action) => {
      state.data =action.payload
    },
    authLogout: (state, action) => {
      state.data = null;
    },
  },
});

export const { authSuccess, authLogout } = authSlice.actions;

export default authSlice.reducer;

this is my user slice file

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { showToast } from "../../utilities/toast";
import { authLogout } from "../auth/authSlice";
import { axiosInstance } from "../../utilities/useAxiosInstance";



const initialState = {
  update: { loading: false, data: null, error: null },
  get: { loading: false, data: null, error: null },
  delete: { loading: false, data: null, error: null },
};

export const getUser = createAsyncThunk("user/getUser", (payload) => {
  const { data, mode } = payload;
  return axiosInstance
    .get(`/user/${data?._id}`, {
      headers: {
        "Content-Type": "application/json",
        Authorization: "Bearer " + data.accessToken,
      },
    })
    .then((res) => res.data)
    .catch((error) => {
      showToast(error.response.data.message, mode, "error");
      throw error.response.data.message;
    });
});

export const deleteUser = createAsyncThunk("user/deleteUser", (payload) => {
  const { auth, navigate, mode, dispatch } = payload;

  return axiosInstance
    .delete(`/user/${auth._id}`, {
      headers: {
        "Content-Type": "application/json",
        Authorization: "Bearer " + auth.accessToken,
      },
    })
    .then((res) => {
      showToast("Deleted Successfully!", mode, "success");
      dispatch(authLogout());
      localStorage.removeItem("userData");
      navigate("/");
    })
    .catch((err) => {
      showToast(err?.response?.data?.message, mode, "error");
      throw err.response.data.message;
    });
});

export const editUser = createAsyncThunk("user/editUser", (payload) => {
  const { form, data, mode } = payload;

  return axiosInstance
    .put(`/user/${data._id}`, form, {
      headers: {
        "Content-Type": "multipart/form-data",
        // Authorization: "Bearer " + data.accessToken,
      },
    })
    .then((res) => {
      showToast("Updated Successfully!", mode, "success");
    })
    .catch((err) => {
      showToast(err?.response?.data?.message, mode, "error");
      throw err.response.data.message;
    });
});

const userSlice = createSlice({
  name: "user",
  initialState,
  extraReducers: (builder) => {
    builder.addCase(getUser.pending, (state, action) => {
      state.get.loading = true;
    });
    builder.addCase(getUser.fulfilled, (state, action) => {
      state.get.loading = false;
      state.get.data = action.payload;
    });
    builder.addCase(getUser.rejected, (state, action) => {
      state.get.loading = false;
      state.get.error = action.payload;
    });
    builder.addCase(editUser.pending, (state, action) => {
      state.update.loading = true;
    });
    builder.addCase(editUser.fulfilled, (state, action) => {
      state.update.loading = false;
      state.update.data = action.payload;
    });
    builder.addCase(editUser.rejected, (state, action) => {
      state.update.loading = false;
      state.update.error = action.payload;
    });
    builder.addCase(deleteUser.pending, (state, action) => {
      state.delete.loading = true;
    });
    builder.addCase(deleteUser.fulfilled, (state, action) => {
      state.delete.loading = false;
      state.delete.data = action.payload;
    });
    builder.addCase(deleteUser.rejected, (state, action) => {
      state.delete.loading = false;
      state.delete.error = action.payload;
    });
  },
});

export default userSlice.reducer;

since i am calling getUser api on page load its calling the custom hook but for edit and delete only till the access token exists its working and rest of the time , my new token is not getting updated

2

Answers


  1. You issue is,the userData within the scope of this interceptor will not automatically update when you fetch new tokens because it’s set outside of the interceptor function. This can lead to the old token being used repeatedly, causing the problems you’re seeing.

    You can modify your interceptor as following,

    axiosInstance.interceptors.request.use(
      async (config) => {
        // Retrieve the latest userData from localStorage
        let currentUserData = localStorage.getItem("userData")
          ? JSON.parse(localStorage.getItem("userData"))
          : null;
    
        // Decode the token to check its expiration
        const accessTokenExpiryDate = jwtDecode(currentUserData?.accessToken);
    
        // If the token has expired, refresh it
        if (accessTokenExpiryDate.exp * 1000 < new Date().getTime()) {
          const { accessToken, refreshToken } = await getTokens(
            JSON.stringify({ token: currentUserData.refreshToken })
          );
    
          // Update userData with the new tokens
          currentUserData = {
            ...currentUserData,
            accessToken,
            refreshToken,
          };
    
          // Save the updated userData to localStorage
          localStorage.setItem("userData", JSON.stringify(currentUserData));
    
          // Update the Authorization header with the new token
          config.headers["Authorization"] = "Bearer " + accessToken;
        } else {
          // If the token hasn't expired, use the current token
          config.headers["Authorization"] = "Bearer " + currentUserData.accessToken;
        }
        
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );
    
    Login or Signup to reply.
  2. you are managing and updating the user data (which includes access tokens) in your useAxiosInstance hook. Let’s review your code:

    const useAxiosInstance = () => {
      const getTokens = async (data) => {
        try {
          const res = await axios.post(
            `${process.env.REACT_APP_API_URL}refresh`,
            data,
            {
              headers: {
                "Content-Type": "application/json",
              },
            }
          );
          return res.data;
        } catch (err) {
          return err;
        }
      };
    
      axiosInstance.interceptors.request.use(
        async (config) => {
          const accessTokenExpiryDate = jwtDecode(userData?.accessToken);
    
          if (accessTokenExpiryDate.exp * 1000 < new Date().getTime()) {
            const { accessToken, refreshToken } = await getTokens(
              JSON.stringify({ token: userData.refreshToken })
            );
    
            userData = {
              ...userData,
              accessToken,
              refreshToken,
            };
            localStorage.setItem("userData", JSON.stringify(userData));
            config.headers["Authorization"] = "Bearer " + accessToken;
          }
          return config;
        },
        (error) => Promise.reject(error)
      );
    
      return userData;
    };
    

    The issue might be related to the fact that you are returning userData directly from the hook, and it might not be updating when tokens are refreshed. The userData is assigned the initial value retrieved from local storage only once when the hook is initialized. Subsequent updates to userData inside the interceptor might not reflect in the return value.

    To fix this, you should update the state inside the hook and return that state. Here’s how you can modify your hook:

    import { useState } from 'react';
    
    const useAxiosInstance = () => {
      const [userData, setUserData] = useState(localStorage.getItem("userData")
        ? JSON.parse(localStorage.getItem("userData"))
        : null);
    
      const getTokens = async (data) => {
        try {
          const res = await axios.post(
            `${process.env.REACT_APP_API_URL}refresh`,
            data,
            {
              headers: {
                "Content-Type": "application/json",
              },
            }
          );
          return res.data;
        } catch (err) {
          return err;
        }
      };
    
      axiosInstance.interceptors.request.use(
        async (config) => {
          const accessTokenExpiryDate = jwtDecode(userData?.accessToken);
    
          if (accessTokenExpiryDate.exp * 1000 < new Date().getTime()) {
            const { accessToken, refreshToken } = await getTokens(
              JSON.stringify({ token: userData.refreshToken })
            );
    
            const updatedUserData = {
              ...userData,
              accessToken,
              refreshToken,
            };
            localStorage.setItem("userData", JSON.stringify(updatedUserData));
            setUserData(updatedUserData);
            config.headers["Authorization"] = "Bearer " + accessToken;
          }
          return config;
        },
        (error) => Promise.reject(error)
      );
    
      return userData;
    };
    

    With this change, the userData state will be updated whenever tokens are refreshed, and the updated state will be returned from the hook. This should ensure that your application uses the latest tokens after refresh.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search