简体   繁体   中英

How can I sequence redux actions after some async action completed in redux-observable?

Context

I have a lot of actions of the form: LOAD_XYZ , LOAD_XYZ_FAIL , LOAD_XYZ_SUCCESS for different page elements that need to be loaded.

I frequently wish to perform a redirect ( react-router ) after some item gets loaded, but since redux-observable does not return a promise I cannot perform the redirect in my component - but instead I have to do it using redux and push('...') from react-router-redux .

That unfortunately leads to a lot of duplication as I have LOAD_XYZ and LOAD_XYZ_REDIRECT_TO versions of the same action, plus two epics for each.

This is starting to feel wasteful and with too much redundancy

const fetchUserEpic = action$ =>
  action$.ofType(FETCH_USER)
    .mergeMap(action =>
      ajax.getJSON(`https://api.github.com/users/${action.payload}`)
      .map(response => fetchUserFulfilled(response))
    );

const fetchUserAndRedirectEpic = action$ =>
  action$.ofType(FETCH_USER_AND_REDIRECT)
    .mergeMap(action =>
      ajax.getJSON(`https://api.github.com/users/${action.payload}`)
      .concatMap(response => [fetchUserFulfilled(response), push(action.redirectTo)])
    );

Problem

Is there some sort of pattern/approach where I can sequence actions to be dispatched after some kind of async request has completed, to avoid redundancy and having to implement multiple versions of the same epic.

For example, I would like instead to have a separate REDIRECT_TO action that I can sequence after the item was loaded.

Something like these (imaginary) solutions:

dispatch(fetchUser(...)).then(redirectTo("..."))
dispatchSequence(fetchUser(...), redirectTo("..."))

I know redux-thunk can do this but then I miss out on all the rxjs operators.

I'm probably trenching into dangerous grounds but I did find a way to do this.

First we make a middleware which will add a completion observable as metadata to actions.

import { Subject } from "rxjs";

export const AddListenerMiddleware = store => next => action => {
  const forkedAction = Object.assign({}, action, { meta: { listener: new Subject()}});
  next(forkedAction);
  return forkedAction.meta.listener.asObservable();
};

Since this is a Subject we can call next on it to emit some value. Creating a notify helper allows us to notify any subscribers of when the action has completed (plus pass down the data).

// notify doesn't consume but just emits on the completion observable
const notify = (action, fn) => source => 
  Observable.create(subscriber => 
    source.subscribe(emmission => {
      if (action.meta && action.meta.listener) {
        action.meta.listener.next(fn(emmission));
      }
      subscriber.next(emmission);
    })
  );

const fetchUserEpic = action$ =>
  action$.ofType(FETCH_USER)
    .mergeMap(action =>
      ajax.getJSON(`https://api.github.com/users/${action.payload}`)
      .map(response => fetchUserFulfilled(response))
      .pipe(notify(action, response => response.data))
    );

And now our dispatch returns an observable on which we can do standard rxjs operations. So this finally this becomes possible:

dispatch(fetchUser(...))
    .subscribe(({id}) => dispatch(redirectTo("/" + id)))

Disclaimer: Use at your own risk!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM