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:
-
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.
- If
-
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).
-
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.
- If the
-
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
- You’ll need to ensure that any Redux state or context (e.g.,
✅ 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
).
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
orsetInterval
) 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
-
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.
-
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. -
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.
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 |