Angular + ngrx : Gestion des requêtes http avec ngrx/effects…

Photo by Ricardo Rocha on Unsplash

TL/DR;

Pour découper correctement une application dont les données sont dans le store, il faut gérer la partie appel de service en tant qu’effet de bord du store et non depuis nos composants. Pour cela, l’équipe ngrx a créé la librairie ngrx/effects dont le but est de réagir aux actions dispatchées vers notre store et ainsi déclencher des actions complémentaires ou liées. Par exemple, dispatcher une action initStore va déclencher un effect qui ira chercher nos données sur le serveur puis les intégrera à notre store.

Prérequis

  • Des connaissances Angular 2+
  • Des connaissances sur ngrx/store et sur l’implémentation d’un store dans angular pour gérer “l’application state”.

Introduction

La mise en place d’un store dans une SPA afin d’avoir une unique “source of truth” de données est un pattern qui améliore efficacement la gestion de l’application et de la mise à jour des vues. Mais même si l’intégration du store est très simple, la gestion des requêtes, du routings, etc. n’est pas toujours triviale pour quelqu’un qui débarque sur le sujet. Voici une petite explication pour gérer vos requêtes serveur ou autres évenements qui devraient se déclencher lorsqu’une action spécifique est dispatchée.

Initialiser le store avec les services

La première chose que vous allez certainement faire, c’est mettre en place le reducer du store et les tests associés. Pour une question de simplicité, ici on ne va pas s’occuper de la partie testing.

// snippet from myApp.reducer.ts
export interface IAppState{
data: string;
isLoading: boolean;
}
export const initialState: IAppState = {
data: "",
isLoading: false
};
export function userReducer(
state: IAppState = initialState,
action: Action
): IAppState{
  switch (action.type) {
case 'INIT_DATA':
return Object.assign({}, state);
default:
return state;
}

Le reflex suivant sera bien souvent de rajouter un appel de votre vue vers le service pour aller chercher les données à mettre dans le store. Bien que ce n’est pas la méthode à utiliser, c’est bien souvent comme cela qu’on réfléchit lorsque l’on débute avec nos stores. Ca pourrait ressembler à quelque chose comme ça :

// Anti-pattern d’exemple à ne pas utiliser
// snippet from MyDataViewer.component.ts
onInit() {
this.myService.getData()
.subscribe(
(payload) => this.store.dispatch({type: 'INIT_DATA', payload})
);
}

Cette solution va fonctionner, mais on perd une des raisons d’avoir un store et qui est le découplage entre la gestion des données et les composants de vues.

Pour améliorer ceci, il va donc falloir déléguer la gestion des données via le service au store.

ngrx/effects

L’équipe ngrx a créé une autre librairie qui s’intègre parfaitement avec ngrx/store : ngrx/effects.

Le but de cette librairie est de réagir aux actions dispatchées vers votre store, et ainsi exécuter automatiquement des fonctionnalitées que vous aurez choisi. Un effect sera déclenché suite à une action, et retournera à son tour une action qui sera automatiquement dispatchée par la librairie. On aura ainsi plusieurs actions chainées alors que notre composant Angular aura dispatché une seule action.

Exemple reprenant notre code précédent :

Notre composant vu ne fera plus d’accès aux données. Il se contentera à présent de dispatcher une action qui ira initialiser le store. On lui enlève ainsi la responsabilité de la gestion des sources de données. Son seul travail sera de demander l’initialisation des données.

// snippet from MyDataViewer.component.ts
onInit() {
this.store.dispatch({ type: 'INIT_DATA' });
}

Notre effect devra lui s’occuper de récupérer cette action et de faire les traitements liés qu’on lui aura demandé. Dans notre cas présent, il devra utiliser le service pour aller initialiser le store :

// snippet from MyData.effect.ts
@Effect()
getEventStart$ = this.actions$
.ofType('INIT_DATA')
.switchMap(payload => this.myService.getData())
.map(responseBody => ({
type: 'INIT_DATA_SUCCESS',
payload: responseBody.json()
}))
.catch((err: Error) => Observable.of(getEventFailed(err))
);

Ici :

  • ofType permettra de déclancher l’effect uniquement sur le type défini.
  • switchMap fera l’appel au service en retournant l’observable lié
  • map retournera, en cas de requête réussie, une action contenant les données renvoyées par le serveur mais en format json
  • catch s’occupera de gérer l’erreur s’il y’en a eu une (ex: http status code 404)

Il ne nous restera à présent plus qu’à mettre en place notre reducer qui aura cette fois 3 actions à gérer au lieu d’une seule. C’est certe plus verbeux, mais structurellement mieux :

// snippet from myApp.reducer.ts
export interface IAppState{
  data: string;
  isLoading: boolean;
}
export const initialState: IAppState = {
  data: "",
  isLoading: false
};
export function userReducer(
    state: IAppState = initialState,
    action: Action
  ): IAppState{
switch (action.type) {
  case 'INIT_DATA':
    return Object.assign({}, state, { isLoading: true });
  case 'INIT_DATA_SUCCESS':
    return Object.assign({}, { data: action.payload, isLoading: false });
  case 'INIT_DATA_FAILED':
    return  Object.assign({}, state, { isLoading: false });
default:
    return state;
}

Et voilà, nous avons à présent :

  • un reducer qui s’occupe de gérer le store
  • un effect qui s’occupe des effets de bord tels que les requêtes http
  • un composant qui s’occupe uniquement de recevoir les données pour les afficher

Pour aller plus loin…

Il existe plusieurs autres librairies qui permettent de gérer d’autres fonctionnalités à partir du store et qui vous sont présenter sur la page de ngrx/platform. On notera par exemple ngrx/router-store pour que votre routing soit également lié à votre application state, ou d’autres intiatives indépendantes au projet ngrx, comme ngrx-store-localstorage.

Ceux-ci feront peut-être l’objet d’un prochain article :)