The use case is I need to check if the token has been expired or not. If the token is expired then I need to call api to get new access token. Access token expires on every 30 minutes. For this what I tried to do is.
- Write a custom middleware called
checkTokenExpirationMiddleware
on store.js - Dispatch
checkToken
action so that saga can catch that action and trigger an api call to get new access token. - Inject reducer and saga on app.js
The issue is saga could not listen that action.
Here is the code on what i did
store/index.js
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import { createInjectorsEnhancer, forceReducerReload } from "redux-injectors";
import createSagaMiddleware from "redux-saga";
const checkTokenExpirationMiddleware = (store) => (next) => (action) => {
console.log("#####action#####", action);
const token = JSON.parse(localStorage.getItem("token"));
if (token && jwt_decode(token).exp < Date.now() / 1000) {
// dispatch checkToken action so that saga will listen this action
// and call api for another access token
const result = next(actions.checkToken());
return result;
// store.dispatch(actions.checkToken());
}
next(action);
};
export function configureAppStore() {
const reduxSagaMonitorOptions = {};
const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
const { run: runSaga } = sagaMiddleware;
// // Create the store with saga middleware
const middlewares = [sagaMiddleware, checkTokenExpirationMiddleware];
const enhancers = [
createInjectorsEnhancer({
createReducer,
runSaga,
}),
];
const store = configureStore({
reducer: createReducer(),
middleware: [...getDefaultMiddleware(), ...middlewares],
devTools:
/* istanbul ignore next line */
process.env.NODE_ENV !== "production" ||
process.env.PUBLIC_URL.length > 0,
enhancers,
});
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept("./reducers", () => {
forceReducerReload(store);
});
}
return store;
}
store/globalSaga.js
export function* checkToken() {
console.log("checkToken sagaƄ");
debugger;
// need to call api to get new access token
}
export function* globalSaga() {
// watches for
console.log("global saga watcher");
yield all([
takeLatest(actions.logout, logout),
takeEvery(actions.checkToken, checkToken),
]);
}
App.js
function App() {
// const dispatch = useDispatch();
console.log("injecting saga and reducer");
useInjectReducer({ key: sliceKey, reducer });
useInjectSaga({ key: sliceKey, saga: globalSaga });
// React.useEffect(() => {
// this executes saga
// console.log("dispatching");
// dispatch(actions.checkToken());
// }, []);
return (
<React.Suspense fallback={<LoadingSpinner />}>
<Router>
<RouterApp />
</Router>
</React.Suspense>
);
}
why after dispatching checkToken
action from middleware, the saga could not catch it?
Note: I am using reduxjs/toolkit.
UPDATE
I fetched api from here to get the new access token. Now i can get access token and there is no infinite loop. However there is a problem, it does not execute the api which was previously failed due to invalid token(expired). How can i now again execute that api after getting fresh access token?
const checkTokenExpirationMiddleware = ({ dispatch, getState }) => (
next
) => async (action) => {
const isAlreadyFetchingAccessToken = getState()?.global
.isAlreadyFetchingAccessToken;
const user = JSON.parse(localStorage.getItem("userInfo"));
const access_token = JSON.parse(localStorage.getItem("access_token"));
if (
!isAlreadyFetchingAccessToken &&
access_token &&
jwt_decode(access_token).exp < Date.now() / 1000
) {
const payload = {
data: {
refresh_token: access_token,
},
};
const response = await request(
"https://localhost:3000/token/refresh",
payload,
"POST"
);
console.log("#########response#########", response);
if (response?.status === 200) {
localStorage.setItem("token", JSON.stringify(response?.access_token));
dispatch(actions.checkTokenSuccess());
next(action); // now need to call the api with new access token which could not execute due to invalid token(expired)
console.log("action dispatching", action);
}
}
next(action);
};
It's because middleware form a pipeline, and the
storeAPI.dispatch
andnext
functions do different thingsstoreAPI.dispatch(action)
sends the action to the start of the middleware chain, so it will go through all middleware (including this one)next(action)
sends the action to the next middleware in the chain.So, if you set things up as
applyMiddleware(sagaMiddleware, myCustomMiddleware)
, and your custom middleware callsnext(action)
, the action will go straight to the reducers and the saga middleware will never see it.If you call
storeAPI.dispatch(action)
instead, it will go to the start of the middleware pipeline, which issagaMiddleware
.