Explosion Pills
Explosion Pills

Reputation: 191749

Cancel observable based on payload rather than effect

I have a service that makes http requests to a backend I don't control to get marketing page contents. Sometimes, I need to load more than one piece of marketing content at the same time. I can create an effect that calls the service.

@Effect()
marketingContent$ = this.actions$
  .ofType(LOAD_MARKETING_CONTENT)
  .switchMap(({ payload }) => this.marketingService.getContent(payload)
    .map(content => Action.LoadMarketingContentComplete(content))
  )

This works fine, and I can call store.dispatch(Action.LoadMarketingContent('A')).

The problem is that if I need to load more than one piece of marketing content at a time the .switchMap will cancel the previous request.

store.dispatch(Action.LoadMarketingContent('A'));
store.dispatch(Action.LoadMarketingContent('B'));
// Only `'B'` is loaded since 'A' gets canceled before it completes

I can use .mergeMap instead of .switchMap, but then duplicate requests won't get canceled.

I can also use separate actions to load each piece of marketing content, but that would require creating an action and effects for each piece.

Is there a way that I can use .switchMap to cancel only requests for the same content (where payload is the same?) or another way to make simultaneous different requests while canceling duplicate requests in the same stream?

Upvotes: 6

Views: 1064

Answers (2)

Explosion Pills
Explosion Pills

Reputation: 191749

Rather than cancel the effect specifically, you can change the effect to retrieve marketing content in groups and still take advantage of switchMap for canceling subsequent requests.

@Effect()
marketingContent$ = this.actions$
  .ofType(LOAD_MARKETING_CONTENT)
  .switchMap(({ payload }) => forkJoin(
    payload.map(name => this.marketingService.getContent(payload))
  ).map(content => Action.LoadMarketingContentComplete(content))

In this case, the payload would be an array of content names to retrieve instead of an individual name which allows you to load content as groups.

Upvotes: 0

cartant
cartant

Reputation: 58400

If you introduce a CANCEL_MARKETING_CONTENT action you could do something like this with mergeMap:

@Effect()
marketingContent$ = this.actions$
  .ofType(LOAD_MARKETING_CONTENT)
  .mergeMap(({ payload }) => this.marketingService
    .getContent(payload)
    .map(content => Action.LoadMarketingContentComplete(content))
    .takeUntil(this.actions$.ofType(CANCEL_MARKETING_CONTENT))
  );

Basically, that would let you load as many pieces of marketing content as you like, but it would be up to you to cancel any pending loads by dispatching an CANCEL_MARKETING_CONTENT action before you dispatch the LOAD_MARKETING_CONTENT action(s).

For example, to load only piece A, you'd do this:

store.dispatch(Action.CancelMarketingContent());
store.dispatch(Action.LoadMarketingContent('A'));

And to load both pieces A and B, you'd do this:

store.dispatch(Action.CancelMarketingContent());
store.dispatch(Action.LoadMarketingContent('A'));
store.dispatch(Action.LoadMarketingContent('B'));

Actually, there is a similar - but neater - way of doing it and it doesn't involve using another action.

You can use the dispatch of the same action with the same payload as the cancellation trigger. For example:

@Effect()
marketingContent$ = this.actions$
  .ofType(LOAD_MARKETING_CONTENT)
  .mergeMap(({ payload }) => this.marketingService
    .getContent(payload)
    .map(content => Action.LoadMarketingContentComplete(content))
    .takeUntil(this.actions$
      .ofType(LOAD_MARKETING_CONTENT)
      .skip(1)
      .filter(({ payload: next }) => next === payload)
    )
  );

From memory, skip will be needed to skip the action currently being handled by the effect. And the answer assumes that payload is "A" or "B", etc.

Upvotes: 8

Related Questions