19

I have a use case where I need to wait for a sequence of actions before I dispatch another using Redux Observables. I've seen some similar questions but I cannot fathom how I can use these approaches for my given use case.

In essence I want to do something like so:

action$
  .ofType(PAGINATION_CLICKED) // This action occurred.
  .ofType(FETCH_SUCCESS) // Then this action occurred after.
  .map(() => analyticsAction()); // Dispatch analytics.

I would also like to cancel and start that sequence over again if another action of type FETCH_ERROR fires for example.

Matt Derrick
  • 5,434
  • 2
  • 33
  • 49

1 Answers1

29

Great question. The important point is that action$ is a hot/multicast stream of all actions as they are dispatched (it's a Subject). Since it's hot we can combine it multiple times and they'll all be listening to the same stream of actions.

// uses switchMap so if another PAGINATION_CLICKED comes in
// before FETCH_SUCCESS we start over

action$
  .ofType(PAGINATION_CLICKED)
  .switchMap(() =>
    action$.ofType(FETCH_SUCCESS)
      .take(1) // <-------------------- very important!
      .map(() => analyticsAction())
      .takeUntil(action$.ofType(FETCH_ERROR))
  );

So every time we receive PAGINATION_CLICKED we'll start listening to that inner Observable chain that listens for a single FETCH_SUCCESS. It's important to have that .take(1) because otherwise we'd continue to listen for more than one FETCH_SUCCESS which might cause strange bugs and even if not is just generally best practice to only take what you need.

We use takeUntil to cancel waiting for FETCH_SUCCESS if we receive FETCH_ERROR first.


As a bonus, if you decide you want also to do some analytics stuff based on the error too, not only start over, you can use race to indeed race between the two streams. First one to emit, wins; the other is unsubscribed.

action$
  .ofType(PAGINATION_CLICKED)
  .switchMap(() =>
    Observable.race(
      action$.ofType(FETCH_SUCCESS)
        .take(1)
        .map(() => analyticsAction()),
      action$.ofType(FETCH_ERROR)
        .take(1)
        .map(() => someOtherAnalyticsAction())
    )
  );

Here's the same thing, but using race as an instance operator instead of the static one. This is a stylistic preference you can choose. They both do the same thing. Use whichever one is more clear to you.

action$
  .ofType(PAGINATION_CLICKED)
  .switchMap(() =>
    action$.ofType(FETCH_SUCCESS)
      .map(() => analyticsAction())
      .race(
        action$.ofType(FETCH_ERROR)
          .map(() => someOtherAnalyticsAction())
      )
      .take(1)
  );
jayphelps
  • 14,317
  • 2
  • 38
  • 52
  • I almost got there! I ended up with your first example but without the (now obviously critical) `take(1)`. This basically meant the "sequence" was ignored after the first "PAGINATION_CLICKED". Thank you very much, great support for the library :) – Matt Derrick Aug 09 '17 at 14:25
  • Instead of race , is there a way i can indicate that i want both actions to be called/completed before producing the third action? – Nick May 10 '18 at 15:49
  • EDIT: nvm , i think i solved this with the zip operator. – Nick May 10 '18 at 16:02
  • Life-changing tip! – tszarzynski Jun 06 '18 at 11:44
  • wouldn't it better to use exaustMap instead of switchMap in your first code example ? If the method analyticsAction() takes some times and and we received pagination_clicked action in the meantime, it will cancel the method analyticsAction() – John Jun 17 '18 at 14:55
  • @John perhaps. It all depends on what behavior a person wants. You may even want mergeMap. It's all up to you. – jayphelps Jun 17 '18 at 22:33