Skip to content

Token Monitor

When it comes to refreshing tokens in a React app using Redux Toolkit and Axios, the two most common approaches are:

  • 🔁 In Axios interceptors
  • 🔁 Proactively in React hooks

TL;DR — Use Axios interceptors for token refresh

  • ✅ Best practice: Use an Axios interceptor to catch 401 responses and refresh the token transparently.
  • 🔁 Optionally, use a hook to proactively refresh (e.g., before expiry) in special cases like long-lived sessions or silent background refresh.

⚠️ Should you do this? Consider the following:

  1. Security Risk

    • If loginFormData contains sensitive credentials (e.g., username/password), storing or reusing it after login could pose a security risk.
    • Ideally, you shouldn’t store plaintext credentials in memory beyond their immediate use.
  2. User Experience

    • Automatically logging in the user after a refresh failure might confuse the user if credentials have changed or expired.
    • It may also mask issues that should be handled with explicit re-authentication (e.g., prompting the user to log in again).
  3. Silent Failures

    • If the login() call fails again, what will your fallback be? Without a retry limit or error state handling, this can lead to poor UX.
  4. State Consistency

    • You’ll need to ensure that any Redux state or context (e.g., setUser, token storage, etc.) is updated accordingly, and that all requests waiting on a token retry are resumed pro

✅ Goal
  • Persist tokens (and their timestamps) in Redux store using redux-persist
  • Monitor token expiration using exp timestamp
  • Automatically refresh the token before it expires
🧱 Assumptions
  • You store JWT tokens (or auth state) in Redux
  • You persist them with redux-persist
  • You have an API to refresh the token
  • You use RTK Query or thunk-based middleware

⏱️ Create Token Monitor (e.g., in App.js)

You can set up a proactive token refresh interval in your top-level component (e.g., App.jsx).

App.jsx
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import jwtDecode from "jwt-decode";
import { setCredentials, logout } from "./features/auth/authSlice";
import { useRefreshTokenMutation } from "./services/authApi";

const App = () => {
  const dispatch = useDispatch();
  const auth = useSelector((state) => state.auth);
  const [refreshToken] = useRefreshTokenMutation();

  useEffect(() => {
    if (!auth?.accessToken || !auth?.exp) return;

    const refreshThreshold = 60; // seconds before expiration
    const currentTime = Math.floor(Date.now() / 1000);
    const timeUntilExpire = auth.exp - currentTime;

    if (timeUntilExpire < refreshThreshold) {
      // refresh proactively
      refreshToken({ token: auth.refreshToken })
        .unwrap()
        .then((data) => {
          const decoded = jwtDecode(data.accessToken);
          dispatch(
            setCredentials({
              accessToken: data.accessToken,
              refreshToken: data.refreshToken,
              exp: decoded.exp,
              iat: decoded.iat,
              user: data.user,
            })
          );
        })
        .catch(() => {
          dispatch(logout());
        });
    }

    const interval = setInterval(() => {
      const now = Math.floor(Date.now() / 1000);
      const expiresIn = auth.exp - now;

      if (expiresIn < refreshThreshold) {
        refreshToken({ token: auth.refreshToken })
          .unwrap()
          .then((data) => {
            const decoded = jwtDecode(data.accessToken);
            dispatch(
              setCredentials({
                accessToken: data.accessToken,
                refreshToken: data.refreshToken,
                exp: decoded.exp,
                iat: decoded.iat,
                user: data.user,
              })
            );
          })
          .catch(() => {
            dispatch(logout());
          });
      }
    }, 30 * 1000); // check every 30 seconds

    return () => clearInterval(interval);
  }, [auth, dispatch, refreshToken]);

  return <div>{/* Your routes/components */}</div>;
};
Note
  • This approach ensures tokens are refreshed just before expiration, using the exp timestamp.
  • You can tune refreshThreshold to control how early to refresh.
  • Make sure your backend sets proper exp/iat in the JWT.

Theory

RTK Query Standard Approach

No, using RTK Query’s baseQuery approach alone cannot proactively refresh the token before it expires.

Explanation based on search results:
  • The typical RTK Query baseQuery with token refresh logic works reactively: it detects a 401 Unauthorized response from the server (meaning the access token has expired) and then triggers a refresh token call to get a new token before retrying the original request.

  • This approach does not track token expiry time or refresh the token proactively ahead of expiry. Instead, it waits until the server rejects a request due to an expired token.

  • To refresh tokens before expiry, you need additional logic outside of RTK Query’s baseQuery, such as:

    • Storing the token expiry timestamp when you receive the token.
    • Using React hooks or middleware with timers (setTimeout or setInterval) to trigger a refresh token request proactively before the token actually expires.
  • RTK Query’s baseQuery function is synchronous in nature and cannot directly use React hooks (which must be called inside React components or custom hooks), so proactive refresh logic relying on hooks or side effects cannot be implemented purely inside baseQuery.

How to do it with RTK Query
  1. Create a custom baseQuery with re-authentication logic

    Use fetchBaseQuery wrapped inside a custom function that:

    • Executes the original query.
    • Checks if the response is a 401 error (token expired).
    • Calls the refresh token endpoint to get a new access token.
    • Updates the token in your store.
    • Retries the original query with the new token.
  2. Use a mutex to prevent multiple simultaneous refresh calls

    When multiple requests fail with 401 at the same time, use a mutex (e.g., from async-mutex) to ensure only one refresh call happens, preventing race conditions.

  3. Inject the updated token into headers dynamically

    In your baseQuery, read the current token from the store or a token management utility before each request to ensure you always send the latest token.

Example
import { fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { Mutex } from "async-mutex";
import { tokenReceived, loggedOut } from "./authSlice";

const mutex = new Mutex();

const baseQuery = fetchBaseQuery({ baseUrl: "/api" });

const baseQueryWithReauth = async (args, api, extraOptions) => {
  await mutex.waitForUnlock();
  let result = await baseQuery(args, api, extraOptions);

  if (result.error && result.error.status === 401) {
    if (!mutex.isLocked()) {
      const release = await mutex.acquire();
      try {
        const refreshResult = await baseQuery(
          "/refreshToken",
          api,
          extraOptions
        );
        if (refreshResult.data) {
          api.dispatch(tokenReceived(refreshResult.data));
          result = await baseQuery(args, api, extraOptions); // retry original query
        } else {
          api.dispatch(loggedOut());
        }
      } finally {
        release();
      }
    } else {
      await mutex.waitForUnlock();
      result = await baseQuery(args, api, extraOptions);
    }
  }
  return result;
};

Summary

Aspect RTK Query baseQuery approach Proactive refresh (before expiry)
When token is refreshed After receiving 401 Unauthorized response Before token expiry, based on expiry time
Mechanism Detect 401 error, call refresh token API Use timers/hooks/middleware outside baseQuery
Can baseQuery do proactive refresh? No Requires additional app-level logic

Conclusion

You cannot rely on RTK Query’s baseQuery alone to refresh tokens before they expire. The baseQuery approach is the recommended standard for reactive refresh on expiry detection, but proactive refresh requires separate logic implemented outside RTK Query, typically using React hooks or middleware that track token expiry and trigger refresh calls in advance.


Proactive Refresh

Proactive refresh (before expiry) considerations:
  • To refresh the token before expiry (e.g., based on token expiry time), you would need to track the token's expiration timestamp in your app state.
  • Then, you could implement a timer or background task (outside of RTK Query baseQuery) to call the refresh endpoint proactively.
  • RTK Query’s baseQuery mechanism does not inherently support scheduling refreshes before expiry.
  • Hooks or other side-effect management (e.g., setTimeout in a React component or middleware) are typically used for proactive refresh.

So, if you want to refresh tokens before they expire, you will need additional logic outside of RTK Query’s baseQuery, such as React hooks with timers or middleware. The RTK Query baseQuery approach is the standard and recommended way to handle token refresh reactively on expiry.

Summary

Approach When token is refreshed Mechanism
Reactive refresh (RTK Query baseQuery) After receiving 401 Unauthorized response Custom baseQuery detects 401, triggers refresh token API call, then retries original request
Proactive refresh Before token expiry Additional logic outside RTK Query (e.g., React hooks with timers or middleware) to track expiry and refresh token proactively