web-dev-qa-db-fra.com

mise à jour de l'état ngRx et ordre d'exécution des effets

J'ai ma propre opinion sur cette question, mais il vaut mieux vérifier et savoir avec certitude. Merci de faire attention et d'essayer d'aider. C'est ici:

Imaginez que nous envoyons une action qui déclenche des changements d'état et a également des effets attachés. Notre code doit donc faire 2 choses - changer d'état et faire quelques effets secondaires. Mais quel est l'ordre de ces tâches? Les faisons-nous de manière synchrone? Je crois que d'abord, nous changeons d'état puis faisons l'effet secondaire, mais y a-t-il une possibilité qu'entre ces deux tâches se produise autre chose? Comme ceci: nous changeons d'état, puis obtenons une réponse sur la requête HTTP que nous avons faite précédemment et le traitons, puis faisons les effets secondaires.

[edit:] J'ai décidé d'ajouter du code ici. Et aussi je l'ai beaucoup simplifié.

Etat:

export interface ApplicationState {
    loadingItemId: string;
    items: {[itemId: string]: ItemModel}
}

Actions:

export class FetchItemAction implements  Action {
  readonly type = 'FETCH_ITEM';
  constructor(public payload: string) {}
}

export class FetchItemSuccessAction implements  Action {
  readonly type = 'FETCH_ITEM_SUCCESS';
  constructor(public payload: ItemModel) {}
}

Réducteur:

export function reducer(state: ApplicationState, action: any) {
    const newState = _.cloneDeep(state);
    switch(action.type) {
        case 'FETCH_ITEM':
            newState.loadingItemId = action.payload;
            return newState;
        case 'FETCH_ITEM_SUCCESS':
            newState.items[newState.loadingItemId] = action.payload;
            newState.loadingItemId = null;
            return newState;
        default:
            return state;
    }
}

Effet:

@Effect()
  FetchItemAction$: Observable<Action> = this.actions$
    .ofType('FETCH_ITEM')
    .switchMap((action: FetchItemAction) => this.httpService.fetchItem(action.payload))
    .map((item: ItemModel) => new FetchItemSuccessAction(item));

Et voici comment nous expédions FetchItemAction:

export class ItemComponent {
    item$: Observable<ItemModel>;
    itemId$: Observable<string>;

    constructor(private route: ActivatedRoute,
                private store: Store<ApplicationState>) {

        this.itemId$ = this.route.params.map(params => params.itemId);

        itemId$.subscribe(itemId => this.store.dispatch(new FetchItemAction(itemId)));

        this.item$ = this.store.select(state => state.items)
            .combineLatest(itemId$)
            .map(([items, itemId]: [{[itemId: string]: ItemModel}]) => items[itemId])
    }
}

Scénario souhaité:

User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;
switchMap operator in our effect cancells previous request_1 and makes request_2;
get the item_2 in response;
store it under key itemId_2 and make loadingItemId = null.

Mauvais scénario:

User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;  
we receive the response_1 before we made the new request_2 but after loadingItemId changed;
we store the item_1 from the response_1 under the key itemId_2;
make loadingItemId = null;
only here our effect works and we make request_2;
get item_2 in the response_2;
try to store it under key null and get an error

La question est donc simplement de savoir si le mauvais scénario peut réellement se produire ou non?

15
Dmytro Garastovych

Notre code doit donc faire 2 choses - changer d'état et faire quelques effets secondaires. Mais quel est l'ordre de ces tâches? Les faisons-nous de manière synchrone?

Disons que nous distribuons l'action A. Nous avons quelques réducteurs qui gèrent l'action A. Ceux-ci seront appelés dans l'ordre dans lequel ils sont spécifiés dans l'objet transmis à StoreModule.provideStore (). Ensuite, l'effet secondaire qui écoute l'action A se déclenchera ensuite. Oui, c'est synchrone.

Je crois que d'abord, nous changeons d'état puis faisons l'effet secondaire, mais y a-t-il une possibilité qu'entre ces deux tâches se produise autre chose? Comme ceci: nous changeons d'état, puis obtenons une réponse sur la requête HTTP que nous avons faite précédemment et le traitons, puis faisons les effets secondaires.

J'utilise ngrx depuis le milieu de l'année dernière et je n'ai jamais observé que ce soit le cas. Ce que j'ai trouvé, c'est que chaque fois qu'une action est envoyée, elle passe par tout le cycle d'être d'abord gérée par les réducteurs puis par les effets secondaires avant que l'action suivante ne soit gérée.

Je pense que cela doit être le cas puisque redux (dont ngrx a évolué) se présente comme un conteneur d'état prévisible sur leur page principale. En permettant à des actions asynchrones imprévisibles de se produire, vous ne pourriez rien prédire et les outils de développement redux ne seraient pas très utiles.

Édité # 1

Je viens donc de faire un test. J'ai exécuté une action "LONG", puis l'effet secondaire exécuterait une opération qui prend 10 secondes. Entre-temps, j'ai pu continuer à utiliser l'interface utilisateur tout en envoyant davantage de messages à l'État. Enfin, l'effet pour 'LONG' est terminé et expédié 'LONG_COMPLETE'. J'avais tort que les réducteurs et les effets secondaires soient une transaction.

enter image description here

Cela dit, je pense qu'il est toujours facile de prédire ce qui se passe car tous les changements d'état sont toujours transactionnels. Et c'est une bonne chose parce que nous ne voulons pas que l'interface utilisateur se bloque en attendant un appel d'API de longue durée.

Édité # 2

Donc, si je comprends bien, le cœur de votre question concerne le switchMap et les effets secondaires. Fondamentalement, vous demandez si la réponse revient au moment où j'exécute le code réducteur qui exécutera ensuite l'effet secondaire avec switchMap pour annuler la première demande.

J'ai trouvé un test qui, je crois, répond à cette question. Le test que j'ai configuré était de créer 2 boutons. L'un appelé Quick et l'autre appelé Long. Quick enverra "QUICK" et Long enverra "LONG". Le réducteur qui écoute Quick se termine immédiatement. Le réducteur qui écoute Long prendra 10 secondes pour terminer.

J'ai configuré un seul effet secondaire qui écoute à la fois rapide et long. Cela prétend émuler un appel API en utilisant 'of' qui me permet de créer un observable à partir de zéro. Cela attendra ensuite 5 secondes (en utilisant .delay) avant d'envoyer "QUICK_LONG_COMPLETE".

  @Effect()
    long$: Observable<Action> = this.actions$
    .ofType('QUICK', 'LONG')
    .map(toPayload)
    .switchMap(() => {
      return of('').delay(5000).mapTo(
        {
          type: 'QUICK_LONG_COMPLETE'
        }
      )
    });

Pendant mon test, j'ai cliqué sur le bouton rapide, puis immédiatement cliqué sur le bouton long.

Voici ce qui s'est passé:

  • Bouton rapide cliqué
  • "QUICK" est expédié
  • L'effet secondaire démarre un observable qui se terminera en 5 secondes.
  • Bouton long cliqué
  • "LONG" est expédié
  • La manipulation du réducteur LONGUE prend 10 secondes. Au bout de 5 secondes, l'original observable de l'effet secondaire se termine mais n'envoie pas le 'QUICK_LONG_COMPLETE'. Encore 5 secondes passent.
  • L'effet secondaire qui écoute "LONG" fait un switchmap annulant mon premier effet secondaire.
  • 5 secondes passent et 'QUICK_LONG_COMPLETE' est envoyé.

enter image description here

Par conséquent, switchMap s'annule et votre mauvais cas ne devrait jamais arriver.

17
seescode