How to delay one epic until another has emitted a value

六月ゝ 毕业季﹏ 提交于 2019-12-04 14:56:48
jayphelps

One approach is (using pseudo names), when receiving the initial action API_GET_REQUEST you immediately start listening for a single INITIALIZE_FULFILLED, which signals that the the initialization (or whatever) has completed--we'll actually kick it off in a bit. When received, we mergeMap (or switchMap whichever for your use case) that into our call to make the other ajax request and do the usual business. Finally, the trick to kick off the actual initialization we're waiting for is adding a startWith() at the end of that entire chain--which will emit and dispatch the action the other epic is waiting for.

const initializeEpic = action$ =>
  action$.ofType('INITIALIZE')
    .switchMap(() =>
      someHowInitialize()
        .map(() => ({
          type: 'INITIALIZE_FULFILLED'
        }))
    );

const getRequestEpic = (action$, store) =>
  action$.ofType('API_GET_REQUEST')
    .switchMap(() =>
      action$.ofType('INITIALIZE_FULFILLED')
        .take(1) // don't listen forever! IMPORTANT!
        .switchMap(() => {
          let url = store.getState().apiDomain + action.payload;
          return ajax(get(url))
            .map(xhr => ({
              type: types.API_RESPONSE,
              payload: xhr.response
            }))
            .catch(error => Observable.of({
              type: 'API_ERROR',
              payload: error
            })); 
        })
        .startWith({
          type: 'INITIALIZE'
        })
    );

You didn't mention how everything works, so this is just pseudo code that you'll need to amend for your use case.


All that said, if you don't ever call that initialization except in this location, you could also just include that code directly in the single epic itself, or just make a helper function that abstracts it. Keeping them as separate epics usually means your UI code can independently trigger either of them--but it might still be good to separate them for testing purposes. Only you can make that call.

const getRequestEpic = (action$, store) =>
  action$.ofType('API_GET_REQUEST')
    .switchMap(() =>
      someHowInitialize()
        .mergeMap(() => {
          let url = store.getState().apiDomain + action.payload;
          return ajax(get(url))
            .map(xhr => ({
              type: types.API_RESPONSE,
              payload: xhr.response
            }))
            .catch(error => Observable.of({
              type: 'API_ERROR',
              payload: error
            }));
        })
        .startWith({ // dunno if you still need this in your reducers?
          type: 'INITIALIZE_FULFILLED'
        })
    );

I think that you are not using combineLatest the way it is intended. Your example should be written as:

const apiGetRequestEpic = (action$, store) => {
  return (
    Observable.combineLatest(
      action$.ofType('APP_INITIALIZED').take(1),
      action$.ofType('API_GET_REQUEST')
    )
    .map(([_, apiGetRequest]) => apiGetRequest)
    .mergeMap((action) => {
      let url = store.getState().apiDomain + action.payload;
      return ajax(get(url))
        .map(xhr => {
          return {type: types.API_RESPONSE, payload: xhr.response};
        })
        .catch(error => {
          return Observable.of({ type: 'API_ERROR', payload: error });
        });
    })
  );
};

This way, this combineLatest will emit whenever an 'API_GET_REQUEST' is emitted, provided that 'APP_INITIALIZED' has ever been dispatched at least once, or, if not, wait for it to be dispatched.

Notice the .take(1). Without it, your combined observable would emit anytime 'APP_INITIALIZED' is dispatched as well, most likely not what you want.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!