51

I would like to dispatch two actions in one effect. Currently I have to declare two effects to achieve this :

// first effect
@Effect()
action1$ = this.actions$
  .ofType(CoreActionTypes.MY_ACTION)
  .map(res => {
    return { type: "ACTION_ONE"}
  })
  .catch(() => Observable.of({
    type: CoreActionTypes.MY_ACTION_FAILED
  }));

// second effect
@Effect()
action2$ = this.actions$
  .ofType(CoreActionTypes.MY_ACTION)
  .map(res => {
    return { type: "ACTION_TWO"}
  })
  .catch(() => Observable.of({
    type: CoreActionTypes.MY_ACTION_FAILED
  }));

Is it possible to have one action, be the source of two actions via a single effect?

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
Lev
  • 13,856
  • 14
  • 52
  • 84
  • 4
    FYI, with `catch` composed where it is in your effect, if an error occurs, the observable will complete and your effect will cease to work. See [this answer](http://stackoverflow.com/a/41685689/6680611). – cartant Jan 17 '17 at 19:01

6 Answers6

72
@Effect()
loadInitConfig$ = this.actions$
    .ofType(layout.ActionTypes.LOAD_INIT_CONFIGURATION)
    .map<Action, void>(toPayload)
    .switchMap(() =>
        this.settingsService
            .loadInitConfiguration()
            .mergeMap((data: any) => [
                new layout.LoadInitConfigurationCompleteAction(data.settings),
                new meetup.LoadInitGeolocationCompleteAction(data.geolocation)
            ])
            .catch(error =>
                Observable.of(
                    new layout.LoadInitConfigurationFailAction({
                        error
                    })
                )
            )
    );
Derek Hill
  • 5,965
  • 5
  • 55
  • 74
Javier González
  • 1,999
  • 17
  • 18
  • 2
    This one is really helpful! – Vasyl Apr 27 '18 at 07:53
  • How about the execute sequence these 2 actions processed by reducer? – Tethys Zhang Jan 13 '20 at 02:05
  • That's the first time I've seen `mergeMap` project function returning an array! Brilliant answer – Drenai Mar 17 '20 at 12:16
  • 4
    This is an anti pattern, according to Mike Ryan the co-founder of ngrx. https://twitter.com/MikeRyanDev/status/938108618133602304 – callback May 25 '20 at 09:09
  • 1
    hi, yes i know, but sometimes an anti-pattern it's not a bad solution – Javier González May 29 '20 at 19:03
  • 1
    This is opinionated, and sometimes it is totally fine to dispatch multiple actions from the same effect, if you know exactly what is affected. – Neurotransmitter Jun 30 '20 at 11:30
  • Returning multiple actions is a nono. Instead you should look into this eslint plugin how it should be done: https://github.com/timdeschryver/eslint-plugin-ngrx/blob/main/docs/rules/no-multiple-actions-in-effects.md – dewey Feb 09 '23 at 11:27
19

You can use switchMap and Observable.of.

 @Effect({ dispatch: true }) action$ = this.actions$
    .ofType(CoreActionTypes.MY_ACTION)
    .switchMap(() => Observable.of(
        // subscribers will be notified
        {type: 'ACTION_ONE'} ,
        // subscribers will be notified (again ...)
        {type: 'ACTION_TWO'}
    ))
    .catch(() => Observable.of({
      type: CoreActionTypes.MY_ACTION_FAILED
    }));

Performance matters :

Instead of dispatching many actions that will trigger all the subscribers as many times as you dispatch, you may want to take a look into redux-batched-actions.

This allows you to warn your subscribers only when all of those multiple actions have been applied to the store.

For example :

@Effect({ dispatch: true }) action$ = this.actions$
    .ofType(CoreActionTypes.MY_ACTION)
    // subscribers will be notified only once, no matter how many actions you have
    // not between every action
    .map(() => batchActions([
        doThing(),
        doOther()
    ]))
    .catch(() => Observable.of({
      type: CoreActionTypes.MY_ACTION_FAILED
    }));
maxime1992
  • 22,502
  • 10
  • 80
  • 121
13

If anyone wonders how to mix plain actions with ones from Observables.

I was stuck with same task, but small difference: I needed to dispatch two actions, with second one after API call, which made it an Observable. Something like:

  1. action1 is just an action: {type: 'ACTION_ONE'}
  2. action2 is API call mapped to action: Observable<{type: 'ACTION_TWO'}>

Following code solved my issue:

@Effect() action$ = this.actions$.pipe(
    ofType(CoreActionTypes.MY_ACTION),
    mergeMap(res =>
        // in order to dispatch actions in provided order
        concat(
            of(action1),
            action2
        )
    ),
    catchError(() => Observable.of({
        type: CoreActionTypes.MY_ACTION_FAILED
    }))
);

concat doc's

Yaremenko Andrii
  • 759
  • 10
  • 9
  • I was in a similar scenario and using `concat` did the work, thanks. My scenario: ` @Effect() action$ = this.actions$.pipe( ofType(CoreActionTypes.MY_ACTION), mergeMap( ( { payload } ) => this.dialog .open(DialogComponent) .afterClosed() .pipe( switchMap((resp) => concat( of( action1 ), action2 ) ) ) ) ); ` – Ricardo Fornes May 28 '20 at 14:15
  • Thanks, I suspected it's doable, but my rxjs knowledge was not strong enough. This worked perfectly – Alma Alma Jul 20 '20 at 11:41
6

You can choose mergeMap for async or concatMap for sync

  @Effect() action$ = this.actions$
    .ofType(CoreActionTypes.MY_ACTION)
    .mergeMap(() => Observable.from([{type: "ACTION_ONE"} , {type: "ACTION_TWO"}]))
    .catch(() => Observable.of({
      type: CoreActionTypes.MY_ACTION_FAILED
    }));
Victor Godoy
  • 1,642
  • 15
  • 18
  • The main problem with this answer is a performance problem (which might just be acceptable) but also a consistency problem, which might be a big deal. If you split your actions and have a normalized state, dispatching 2 actions without using batched actions might end up into a store inconsistency and some selectors of your app might end up with errors. – maxime1992 Sep 20 '17 at 07:11
2

you can also use .do() and store.next()

do allows you to attach a callback to the observable without affecting the other operators / changing the observable

e.g.

@Effect() action1$ = this.actions$
.ofType(CoreActionTypes.MY_ACTION)
.do(myaction => this.store.next( {type: 'ACTION_ONE'} ))
.switchMap((myaction) => Observable.of(
    {type: 'ACTION_TWO'}
))
.catch(() => Observable.of({
  type: CoreActionTypes.MY_ACTION_FAILED
}));

(you will need a reference to the store in your effects class)

JusMalcolm
  • 1,431
  • 12
  • 10
1

How about using flatMap as below with ngrx 8.3.0 version syntax of effects and actions:

  someEffect$ = createEffect(() =>
    this.actions$.pipe(
      ofType(CoreActionTypes.MY_ACTION),
      flatMap((data) => [
        CoreActionTypes.MY_ACTION_2(data),
        CoreActionTypes.MY_ACTION_3(data),
      ])
    )
  );

Shashank Vivek
  • 16,888
  • 8
  • 62
  • 104