web-dev-qa-db-fra.com

Comment attendre 2 actions dans @ ngrx/effect

Peut-on attendre deux actions comme Promise.all? Exemple:

@Effect()
pulic addUser() {
   return this.actions$.ofType(user.ADD)
      .switchMap(() => {
         return this.userService.add();
      })
      .map(() => {
         return new user.AddSuccessAction();
      });
}

@Effect()
pulic addUserOptions() {
   return this.actions$.ofType(userOptions.ADD)
      .switchMap(() => {
         return this.userOptionsService.add();
      })
      .map(() => {
         return new userOptions.AddSuccessAction();
      });
}

@Effect()
public complete() {
   return this.actions$.ofType(user.ADD_SUCCESS, userOptions.ADD_SUCCESS)
      // how to make it works like Promise.all ?
      .switchMap(() => {
         return this.statisticService.add();
      })
      .map(() => {
         return new account.CompleteAction();
      });
}

UPDATED Je souhaite obtenir un comportement similaire à Promise.all. Comment envoyer deux effets en parallèle, attendez que tous les effets soient résolus, puis envoyez une troisième action. Quelque chose comme https://redux-saga.js.org/docs/advanced/RunningTasksInParallel.html Avec des promesses c'était assez évident:

Promise.all([fetch1, fetch2]).then(fetch3);

Est-ce possible dans ngrx/effects? Ou est-ce une mauvaise manière dans ngrx/effects?

RÉPONSE

Il y a peu d'options que vous pouvez utiliser:

1) N'utilisez pas d'actions génériques.

Suivez ces règles de la présentation de Myke Ryan: https://youtu.be/JmnsEvoy-gY

Pros : plus facile à déboguer

Cons : tonnes de passe-partout et actions

2) Utiliser un flux complexe avec des actions imbriquées.

Consultez cet article: https://bertrandg.github.io/ngrx-effects-complex-stream-with-nested-actions/

Voici un exemple simple pour deux actions:

@Effect()
public someAction(): Observable<Action> {
    return this.actions$.pipe(
        ofType(actions.SOME_ACTION),
        map((action: actions.SomeAction) => action.payload),
        mergeMap((payload) => {
            const firstActionSuccess$ = this.actions$.pipe(
                ofType(actions.FIRST_ACTION_SUCCESS),
                takeUntil(this.actions$.pipe(ofType(actions.FIRST_ACTION_FAIL))),
                first(),
            );

            const secondActionsSuccess$ = this.actions$.pipe(
                ofType(actions.SECOND_ACTION_SUCCESS),
                takeUntil(this.actions$.pipe(ofType(actions.SECOND_ACTION_FAIL))),
                first(),
            );

            const result$ = forkJoin(firstActionSuccess$, secondActionsSuccess$).pipe(
                first(),
            )
                .subscribe(() => {
                    // do something
                });

            return [
                new actions.FirstAction(),
                new actions.SecondAction(),
            ];
        }),
    );
}

Pros : vous pouvez réaliser ce que vous voulez

Inconvénients : le flux complexe est trop complexe à prendre en charge :) a l'air laid et peut rapidement devenir un enfer, les observables ne se désabonnent pas tant que des actions ne réussissent pas ou échouent, cela signifie qu'en théorie, toute action d'un tiers peut émettre des signaux à ces observables.

3) Utilisez un motif d'agrégation.

Consultez la présentation de Victor Savkin sur les modèles de gestion d'état et les meilleures pratiques avec NgRx: https://www.youtube.com/watch?v=vX2vG0o-rpM

Voici un exemple simple:

Vous devez d’abord créer des actions avec le paramètre correlationId. CorrelationId devrait être uniq, ce peut être un guide par exemple. Cet identifiant que vous utiliserez dans votre chaîne d'actions pour identifier vos actions.

export class SomeAction implements Action {
    public readonly type = SOME_ACTION;

    constructor(public readonly correlationId?: string | number) { }
    // if you need payload, then make correlationId as a second argument
    // constructor(public readonly payload: any, public readonly correlationId?: string | number) { }
}

export class SomeActionSuccess implements Action {
    public readonly type = SOME_ACTION_SUCCESS;

    constructor(public readonly correlationId?: string | number) { }
}

export class FirstAction implements Action {
    public readonly type = FIRST_ACTION;

    constructor(public readonly correlationId?: string | number) { }
}

export class FirstActionSuccess implements Action {
    public readonly type = FIRST_ACTION_SUCCESS;

    constructor(public readonly correlationId?: string | number) { }
}

// the same actions for SecondAction and ResultAction

Ensuite nos effets:

@Effect()
public someAction(): Observable<Action> {
    return this.actions$.pipe(
        ofType(actions.SOME_ACTION),
        mergeMap((action: actions.SomeAction) => {
            return [
                new actions.FirstAction(action.corelationId),
                new actions.SecondAction(action.corelationId),
            ];
        }),
    );
}

@Effect()
public firstAction(): Observable<Action> {
    return this.actions$.pipe(
        ofType(actions.FIRST_ACTION),
        switchMap((action: actions.FirstAction) => {
            // something
            ...map(() => new actions.FirstActionSuccess(action.correlationId));
        }),
    );
}
// the same for secondAction

@Effect()
public resultAction(): Observable<Action> {
    return this.actions$.pipe(
        ofType(actions.SOME_ACTION),
        switchMap((action: actions.SomeAction) => {
            const firstActionSuccess$ = this.actions$.pipe(
                ofType(actions.FIRST_ACTION_SUCCESS),
                filter((t: actions.FirstActionSuccess) => t.correlationId === action.correlationId),
                first(),
            );

            const secondActionsSuccess$ = this.actions$.pipe(
                ofType(actions.SECOND_ACTION_SUCCESS),
                filter((t: actions.SecondActionSuccess) => t.correlationId === action.correlationId),
                first(),
            );

            return Zip(firstActionSuccess$, secondActionsSuccess$).pipe(
                map(() => new actions.resultSuccessAction()),
            )
        }),
    );
}

Pros : identique au point 2, mais pas d'action de tiers.

Cons : identique aux points 1 et 2

4) N'utilisez pas d'effets pour l'API. Utilisez de bons vieux services qui imitent les effets mais qui retournent Observable.

Dans votre service:

public dispatchFirstAction(): Observable<void> {
    this.store.dispatch(new actions.FirstAction(filter));

    return this.service.someCoolMethod().pipe(
        map((data) => this.store.dispatch(new actions.FirstActionSuccess(data))),
        catchError((error) => {
            this.store.dispatch(new actions.FirstActionFail());

            return Observable.throw(error);
        }),
    );
}

Donc, vous pouvez le combiner n'importe où plus tard, comme:

const result1$ = this.service.dispatchFirstAction();
const result2$ = this.service.dispatchSecondAction();

forkJoin(result1$, result2$).subscribe();

5) Utilisez ngxs: https://github.com/ngxs/store

Pros : moins de passe-partout, on se croirait dans un angle, ça pousse vite

Cons : a moins de fonctionnalités que ngrx

7
E. Efimov

Je suis nouveau à RXJS, mais qu'en est-il de cela? 

Vous pouvez supprimer {dispatch: false} si vous changez la tap en une switchMap.

@Effect({dispatch: false})
public waitForActions(): Observable<any> {
    const waitFor: string[] = [
        SomeAction.EVENT_1,
        SomeAction.EVENT_2,
        SomeAction.EVENT_3,
    ];

    return this._actions$
        .pipe(
            ofType(...waitFor),
            distinct((action: IAction<any>) => action.type),
            bufferCount(waitFor.length),
            tap(console.log),
        );
}
4
codeBelt

Utiliser Observable.combineLatest fonctionne pour moi.

@Effect()
  complete$ = this.actions$.ofType<Action1>(ACTION1).combineLatest(this.actions$.ofType<Action2>(ACTION2),
    (action1, action2) => {

      return new Action3();
    }
  ).take(1);

take (1) entraîne l'envoi d'Action3 () une seule fois.

3
deumax

Une autre version combineLatest avec pipes et switchMap

import { Observable, of } from 'rxjs'
import { combineLatest, switchMap, withLatestFrom } from 'rxjs/operators'

@Effect()
someEffect$: Observable<Actions> = this.actions$.pipe(
  ofType(Action1),
  combineLatest(this.actions$.ofType(Action2)),
  switchMap(() => of({ type: Action3 }))
)
0