MistyK
MistyK

Reputation: 6222

React Redux with Redux Observable wait for authentication action to finish before continuing

My background is .NET development, WPF precisely. When you wanted to call an authenticated action in C# this is what you roughly have to do.

var res = action();
if(res == Not authenticated)
{
  var cred = await showDialog();
  action(cred);
}

Pretty simple, call an action, if you are not authenticated show dialog window where user can enter credentials and call action with credentials again.

Now I'm trying to implement the same functionality in redux with epics.

export const pullCurrentBranchEpic = (action$: ActionsObservable<Action>, store: Store<ApplicationState>) =>
  action$
    .filter(actions.pullCurrentBranch.started.match)
    .mergeMap(action => Observable.merge(
      Observable.of(repoActions.authenticate.started({})),
      waitForActions(action$, repoActions.authenticate.done)
                  .mergeMap(async _=>{
                    const repository = await getCurrentRepository(store.getState());
                    await pullCurrentBranch(repository);
                  })
                  .map(_=>actions.pullCurrentBranch.done({params:{},result:{}}))
                  .race(
                    action$.ofType(repoActions.authenticate.failed.type)
                      .map(() => actions.pullCurrentBranch.failed({error:"User not authenticated",params:{}}))
                      .take(1))
    ));
export const waitForActions = (action$, ...reduxActions) => {
  const actionTypes = reduxActions.map(x => x.type);
  const obs = actionTypes.map(type => action$.ofType(type).take(1));
  return Observable.forkJoin(obs);
};

Basically epic will handle an action (I don't even check if user should be authenticated or not because it will double this code) and action will run another action - authenticate.started which will change a state in a reducer to open the window, user will be able to enter his credentials and when he does it, it will call authenticate.done action. When it's done, epic will continue with a promise to call an action which will raise another action when it's finished. Am I doing something wrong? Isn't it just too complicated for such a simple thing? I don't think it's an issue with redux but rather redux observable and epics.

Upvotes: 0

Views: 893

Answers (1)

Robert Farley
Robert Farley

Reputation: 2456

This might fit your needs. The important thing to note is breaking down the problem into multiple epics.

const startAuthEpic = action$ => action$
  .filter(action.pullCurrentBranch.start.match)
  .map(action => repoActions.authenticate.started({});

const authDoneEpic = (action$, store) => action$
  .ofType(repoActions.authenticate.done.type)
  .mergeMap(() => {
    const repo = getCurrentRepository(store.getState());
    return Observable.fromPromise(pullCurrentBranch(repo))
      .map(() => actions.pullCurrentBranch.done({ ... }))
      .catch(error => Observable.of(actions.pullCurrentBranch.failed({ ... })))
  });

There are multiple ways to include more authenticated actions, one being:

// each new authenticated action would need to fulfill the below contract
const authenticatedActions = [
  {
    action: 'pull',
    match: action.pullCurrentBranch.start.match,
    call: store => {
      const repo = getCurrentRepository(store.getState());
      return pullCurrentBranch(repo);
    },
    onOk: () => actions.pullCurrentBranch.done({}),
    onError: () => actions.pullCurrentBranch.failed({})
  },
  {
    action: 'push',
    match: action.pushCurrentBranch.start.match,
    call: store => {
      const repo = getCurrentRepository(store.getState());
      return pushCurrentBranch(repo);
    },
    onOk: () => actions.pushCurrentBranch.done({}),
    onError: () => actions.pushCurrentBranch.failed({})
  }
];

const startAuthEpic = action$ => action$
  .map(action => ({ action, matched: authenticatedActions.find(a => a.match(action)) }))
  .filter(wrapped => wrapped.matched)
  .map(wrapped => repoActions.authenticate.started({ whenAuthenticated: wrapped.matched }));

const authDoneEpic = (action$, store) => action$
  .ofType(repoActions.authenticated.done.type)
  .pluck('whenAuthenticated')
  .mergeMap(action => {
    const { call, onOk, onError } = action;
    return Observable.fromPromise(call(store))
      .map(onOk)
      .catch(err => Observable.of(onError(err)))
  });

You would just have to pass along the whenAuthenticated property to the authenticate.done action.

Upvotes: 2

Related Questions