I have a React/redux/electron app that uses Google Oauth. I want to be able to refresh the access token automatically when it expires. I've researched this and solved it semi-successfully using middleware, but my solution is erroring in certain situations.
I've implemented a refresh middleware that runs on every API action. It checks whether the access token is expired or about to expire. If so, instead of dispatching the action it received, it dispatches a token refresh action and queues up any other actions until a new access token is received. After that, it dispatches all actions in its queue.
However, one of my action creators looks something like this:
function queryThreads(params) {
return async (dispatch) => {
const threads = await dispatch(fetchThreads(params))
const newPageToken = threads.payload.nextPageToken
}
}
When the refresh middleware doesn't run because the token isn't expiring, threads.payload
will be defined here and everything will work as intended.
However, when the refresh middleware does run, threads.payload
will be undefined
because the dispatch
seems to resolve with the value of the token refresh action, rather than the fetchThreads
action.
How do I ensure that the token gets refreshed (and updated in state
/localStorage
), fetchThreads
gets dispatched with the updated token, and the threads
variable gets assigned to the resolved value of the correct Promise?
This is my refresh middleware. It was inspired by this article by kmmbvnr.
This is the token refresh action creator.
This is the line in my queryThreads action creator that throws when the token has to refresh (threads.payload
is undefined
).
This is the reducer where I update state in response to a token refresh.
This is the middleware where I update localStorage in response to a token refresh.
It looks like I've solved the issue by rewriting the refresh middleware like this:
function createRefreshMiddleware() {
const postponedRSAAs = [];
return ({ dispatch, getState }) => {
const rsaaMiddleware = apiMiddleware({ dispatch, getState });
return next => action => {
if (isRSAA(action)) {
try {
const auth = JSON.parse(localStorage.getItem('auth'));
const { refresh_token: refreshToken } = auth;
const expirationTime = jwtDecode(auth.id_token).exp * 1000;
const isAccessTokenExpiring =
moment(expirationTime) - moment() < 300000;
if (refreshToken && isAccessTokenExpiring) {
postponedRSAAs.push(action);
if (postponedRSAAs.length === 1) {
return rsaaMiddleware(next)(
dispatch(() => attemptTokenRefresh(refreshToken))
).then(() => {
const postponedRSAA = postponedRSAAs.pop();
return dispatch(postponedRSAA);
});
}
return rsaaMiddleware(next)(action);
}
return rsaaMiddleware(next)(action);
} catch (e) {
console.log(e);
return next(action);
}
}
return next(action);
};
};
}
export default createRefreshMiddleware();
Now the postponed action will always be chained off of the token refresh action, so we don't have the problem of the original promise resolving with the wrong value; plus it's more concise.