user17472
user17472

Reputation: 139

in redux observable, how do i fire an action before any other action

Background:

i am using epics for managing requests.

for every request i send a token, that may expire, but can be refreshed within a grace period.

i am using the token for every request, but before sending any request i want to check if the token is expired or not, and if expired and with grace period, then first refresh the token and then proceed with the corresponding action

all the requests have their own epics.

Now what i trying is a pre hook on all actions to check token possibly refresh it and then proceed with actions.

Hope this explains.

// epics for querying data
// these epics are using a token, that is stored in redux state.

const getMenuEpic = action$ => ....

const getFoodListEpic = action$ => ....

const getFoodItemEpic = action$ => ....

...


// helper function to check 
// if token has expired

const hasTokenExpired = (token) => .....

// refresh token 
// this returns a new token in the promise

const refreshToken = fetch('http://.../refresh-toekn')

// i am trying to make an epic, that will fire 
// before any actions in the application
// stop the action (let's call it action A)
// get token from redux state, 
// verify if is valid or not
// if invalid call refresh token (async process), 
// and when refresh token finished, proceed with the incoming action A
// if the token was valid then continue with action A.

const refreshEpic = (action$, store) => 
 action$.map(() => store.getState().token)
  .filter(Boolean)
  .filter(token => hasTokenExpired(token))
  .mergeMap(() => refreshToken()) ...

 ......

but this approach is not working for refreshEpic

Upvotes: 1

Views: 2416

Answers (1)

jayphelps
jayphelps

Reputation: 15401

It's not possible to truly prevent an action from reaching your reducers--it's actually already been through them before it's given to your epics--instead you can dispatch an action which signals the intent to fetch but isn't actually what triggers it. e.g. UI dispatches FETCH_SOMETHING and an epic sees it, confirms there's a valid refresh token (or gets a new one), then emits another action to actually trigger the fetch e.g. FETCH_SOMETHING_WITH_TOKEN.

In this particular case though you'll likely have many epics with the same requirement and it could get tedious to do it that way. There are many ways to make this easier. Here are a couple:

Wrap the fetch in a helper

You could write a helper which does the checking for you and if it needs a refresh it will request and wait for it before proceeding. I would personally handle the actual refresh in separate dedicated epic so that you prevent multiple concurrent requests to refresh and other things like that.

const requireValidToken = (action$, store, callback) => {
  const needsRefresh = hasTokenExpired(store.getState().token);

  if (needsRefresh) {
    return action$.ofType(REFRESH_TOKEN_SUCCESS)
      .take(1)
      .takeUntil(action$.ofType(REFRESH_TOKEN_FAILED))
      .mergeMap(() => callback(store.getState().token))
      .startWith({ type: REFRESH_TOKEN });
  } else {
    return callback(store.getState().token);
  }
};

const getMenuEpic = (action$, store) =>
  action$.ofType(GET_MENU)
    .switchMap(() =>
      requireValidToken(action$, store, token =>
        actuallyGetMenu(token)
          .map(response => getMenuSuccess(response))
      )
    );

"super-epics"

EDIT: This was my original suggestion but it's much more complicated than the one above. It has some benefits too, but IMO the one above will be easier to use and maintain.

You could instead create a "super-epic", an epic which itself composes and delegates to other epics. The root epic is an example of a super-epic. (I just made up that term right now...lol)

One thing we'll probably want to do is differentiate between any random action and ones which require an auth token--you don't want to check for the auth token and refresh it for every single action ever dispatched. A simple way would be to include some metadata in the action, like { meta: { requiresAuth: true } }

This is much more complex, but has pros over the other solution as well. Here's a rough idea of what I'm talking about, but it's untested and probably not 100% thought-out. Consider it inspiration rather than copy-pasta.

// action creator helper to add the requiresAuth metadata
const requiresAuth = action => ({
  ...action,
  meta: {
    ...action.meta,
    requiresAuth: true
  }
});
// action creators
const getMenu = id => requiresAuth({
  type: 'GET_MENU',
  id
});
const getFoodList = () => requiresAuth({
  type: 'GET_FOOD_LIST'
});

// epics
const getMenuEpic = action$ => stuff
const getFoodListEpic = action$ => stuff

const refreshTokenEpic = action$ =>
  action$.ofType(REFRESH_TOKEN)
    // If there's already a pending refreshToken() we'll ignore the new
    // request to do it again since its redundant. If you instead want to
    // cancel the pending one and start again, use switchMap()
    .exhaustMap(() =>
      Observable.from(refreshToken())
        .map(response => ({
          type: REFRESH_TOKEN_SUCCESS,
          token: response.token
        }))
        // probably should force user to re-login or whatevs
        .catch(error => Observable.of({
          type: REFRESH_TOKEN_FAILED,
          error
        }))
    );


// factory to create a "super-epic" which will only
// pass along requiresAuth actions when we have a
// valid token, refreshing it if needed before.
const createRequiresTokenEpic = (...epics) => (action$, ...rest) => {
  // The epics we're delegating for
  const delegatorEpic = combineEpics(...epics);
  // We need some way to emit REFRESH_TOKEN actions
  // so I just hacked it with a Subject. There is
  // prolly a more elegant way to do this but #YOLO
  const output$ = new Subject();

  // This becomes action$ for all the epics we're delegating
  // for. This will hold off on giving an action to those
  // epics until we have a valid token. But remember,
  // this doesn't delay your *reducers* from seeing it
  // as its already been through them!
  const filteredAction$ = action$
    .mergeMap(action => {
      if (action.meta && action.meta.requiresAuth) {
        const needsRefresh = hasTokenExpired(store.getState().token);

        if (needsRefresh) {
          // Kick off the refreshing of the token
          output$.next({ type: REFRESH_TOKEN });

          // Wait for a successful refresh
          return action$.ofType(REFRESH_TOKEN_SUCCESS)
            .take(1)
            .mapTo(action)
            .takeUntil(action$.ofType(REFRESH_TOKEN_FAILED));
            // Its wise to handle the case when refreshing fails.
            // This example just gives up and never sends the
            // original action through because presumably
            // this is a fatal app state and should be handled
            // in refreshTokenEpic (.e.g. force relogin)
        }
      }

      // Actions which don't require auth are passed through as-is
      return Observable.of(action);
    });

  return Observable.merge(
    delegatorEpic(filteredAction$, ...rest),
    output$
  );
};

const requiresTokenEpic = createRequiresTokenEpic(getMenuEpic, getFoodList, ...etc);

As mentioned, there are many ways to approach this problem. I can invision creating some sort of helper function you use inside your epics that require tokens instead of this "super-epic" approach. Do what seems less complicated for you.

Upvotes: 7

Related Questions