Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to delay one epic until another has emitted a value

In redux-observable I need to wait before doing an API request until another epic has completed. Using combineLatest doesn't work, nothing happens after APP_INITIALIZED finishes. I also tried withLatestFrom but neither works.

const apiGetRequestEpic = (action$, store) => {
  return Observable.combineLatest(
    action$.ofType('APP_INITIALIZED'),
    action$.ofType('API_GET_REQUEST')
      .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 });
          });
      })
  );
};

combineLatest definition

like image 595
ddcc3432 Avatar asked Dec 24 '22 20:12

ddcc3432


2 Answers

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'
        })
    );
like image 119
jayphelps Avatar answered Dec 31 '22 12:12

jayphelps


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.

like image 33
deb0ch Avatar answered Dec 31 '22 14:12

deb0ch