0
votes

Update

Here is a working example using redux-observable. https://redux-observable-playground-ykzsyp.stackblitz.io This achieves what I want using mergeMap and if/else statement, but I was hoping to use Observable.filter as that seems more elegant.


Original question

I have an epic that currently dispatches a single action but would like to dispatch different actions based on a filter using a single stream. Here is the current code:

const streamEpic = (action$) => action$.pipe(
    ofType('START_PLAYBACK_STREAM'),
    switchMap(playbackStream$),
    filter((playEvent) => playEvent.type === 'LOAD')),
    map((playEvent) => loadActionCreator(playEvent.fileName))
    // how can I filter on other types? 
    // and dispatch other actions?
);

I've seen many rxjs examples that use a single stream and filter to map different actions, for example:

playbackStream$
    .filter((playEvent) => playEvent.type === 'LOAD'))
    .map((playEvent) => loadActionCreator(playEvent.fileName));

playbackStream$
    .filter((playEvent) => playEvent.type === 'START'))
    .map((playEvent) => startActionCreator());

playbackStream$
    .filter((playEvent) => playEvent.type === 'STOP'))
    .map((playEvent) => stopActionCreator());

I'm trying to do this same thing in redux-observable but no luck. If I use tap, ignoreElements, and store.dispatch I can get the following to work but I know its an anti-pattern.

const streamLoadEvents = (action$, store) => action$.pipe(
    ofType('START_PLAYBACK_STREAM'),
    tap(() => {
        playbackStream$
            .filter((playEvent) => playEvent.type === 'LOAD'))
            .map((playEvent) => store.dispatch(loadActionCreator(playEvent.fileName)));

        playbackStream$
            .filter((playEvent) => playEvent.type === 'START'))
            .map((playEvent) => store.dispatch(startActionCreator()));

        playbackStream$
            .filter((playEvent) => playEvent.type === 'STOP'))
            .map((playEvent) => store.dispatch(stopActionCreator()));
    }),
    ignoreElements()
);

I know that I could also use a switch or if/else statement inside of something like map or switchMap, like the answer here: Redux-Observable multiple actions in single epic, but I'd like to avoid this as its rather inelegant and does not take full advantage of streaming operators. The answer here: https://stackoverflow.com/a/40895613/367766 seems to get me a little closer...

What's the suggested approach here? Are the operators or example you can point me to? Thanks!

1
What's the problem with the answer here stackoverflow.com/questions/40886655/…? I think it describes exactly what you need.martin
Maybe, but I can't get my example to work using that reference. The closest I can get is using mergeMap and if/else, which works fine - I was just hoping to use Observable.filter as it seemed to be more elegant. Here is a playground with a working example: redux-observable-playground-ykzsyp.stackblitz.iouser367766

1 Answers

0
votes

You're right, if/else should only be used as a last resort and there are probably several ways you could approach this, in any case, here's my take on this:

There's a merge method which can be viewed as a reactive equivalent of the if/else operator: you provide it with observables, and if any of them emits, the resulting observable will emit it's own value(AC) as well. When a new action comes through, we inspect it, and pick a new action creator(AC) accordingly. Let's take look at some code:

  const filteredAction$ = action$.pipe(
      ofType('START_PLAYBACK_STREAM'),
  );

Nothing interesting here, just filtering out the action types we're not interested in, and then

  const operation$ = merge(
      filteredAction$.pipe(
          filter((action) => action.payload.type === 'LOAD'),
          mapTo(loadActionCreator),
      ),
      filteredAction$.pipe(
          filter((action) => action.payload.type === 'START'),
          mapTo(startActionCreator),
      ),
      filteredAction$.pipe(
          filter((action) => action.payload.type === 'STOP'),
          mapTo(stopActionCreator),
      ),
  ).pipe(
      startWith(startActionCreator)
  );

Here we are choosing which action creator to use based on the action's payload (please note that I'm following the flux standard for actions - the action's data is under the payload property now, there's an awesome library you could use as a helper for it. Also, you should probably rename the 'type' prop to avoid confusion).

mapTo basically tells the operation$ stream what to emit, you can think of this expression as assigning a function to some variable for later use. Every time an epic gets invoked, we'll choose a proper action creator for it here. startWith is actually not needed here (assuming the first action always includes the 'START' type) and is for semantic purposes only.

Last but not least, an epic has to return at least one action for dispatching:

return combineLatest(
      operation$,
      filteredAction$
  )
      .pipe(
          map((arg) => {
            const operation = arg[0];
            const actionObject = arg[1];
            return operation(actionObject);
          })
      )

Latest values from both streams are combined together to form a follow up action: operation$ (which yields us a proper action creator function) and filteredAction$ (always contains the action itself, we might need some data from it).