Angular & RxJS : fine-grained CRUD status tracking sur une liste de données (thanks to groupBy

L'article montre comment appliquer une requête API à chaque entité d'une liste, afficher son statut de chargement et exécuter les requêtes en parallèle. Tu trouveras un exemple que tu pourras utiliser dans tes applications. Voici un exemple : Pour faire ça facilement, je vais te présenter l'opérateur groupBy que l'on va utiliser avec un autre opérateur custom que j'utilise fréquemment "statedStream". Créer l'opérateur "statedStream" pour connaître le statut de chargement d'une requête L'opérateur statedStream permet de connaître le statu de chargement d'une requête asynchrone, je l'utilise la plupart du temps lors d'un appel API. Le fonctionnement est similaire à celui de httpRessource d'Angular, sauf que l'on reste dans le domaine des observables. updateItem$ .pipe( switchMap((updateItem) => // everytime, updateItem$ emit a new value, it cancels the existing api call, and create a new API statedStream(updateApiCall$(updateItem), updateItem) ) ) .subscribe((data) => console.log('data', data)); Au lieu d'attendre de recevoir une unique valeur lorsque l'appel API se termine, statedStream va émettre une première valeur en indiquant que la requête est en train de charger (isLoading: true). Voici une partie du code de statedStream : export function statedStream( toCall: Observable, initialValue: T ): Observable { return toCall.pipe( map( (result) => ({ isLoading: false, isLoaded: true, hasError: false, error: undefined, result, } satisfies SatedStreamResult) ), startWith({ isLoading: true, isLoaded: false, hasError: false, error: undefined, result: initialValue, }), catchError((error) => of({ isLoading: false, isLoaded: false, hasError: true, error, result: initialValue, }) ) ); } A noter que si tu n'as pas l'habitude de travailler avec des streams d'observables, si l'appel api retourne une erreur, ton stream s'arrête et n'écoutera plus les prochaines émissions de la source (ici updateItem$). Grâce à la fonction statedStream, les erreurs sont "catch" et récupéré dans le résultat. Cela permet de ne pas rompre le stream lors d'une erreur API et de continuer à émettre de nouvelles requêtes. Voici un lien stackblitz pour voir cette fonction en détail Débloquer le potentiel titanesque de l'opérateur groupBy Je ne sais pas si tu as déjà utilisé l'opérateur groupBy de RxJs ? Perso, quand j'ai lu la doc la première fois, j'ai pas compris. La dixième fois non plus... Mais grâce à cet exemple, j'ai compris. Depuis, je comprends ;D Si tu souhaites lire la doc, n'hésite pas, il y a aussi un exemple sur stackblitz. Grosso modo, on réutilise l'exemple de statedStream et on l'ajoute dans le stream du groupBy: updateItem$ .pipe( groupBy((updateItem) => updateItem.id), // create a group for each unique id mergeMap((group$) => { console.log('group$', group$.key); return group$.pipe( switchMap((updateItem) => statedStream(updateApiCall$(updateItem), updateItem) ) ); }) ) .subscribe((data) => console.log('Received:', data)); Ensuite, on émet quelques update et là, tu vas comprendre. On émet 2 update successivement, puis un troisième update après 5s. console.log("emit updateItem first time", 'id: 4') updateItem$.next({ id: '4', name: 'Romain Geffrault 4', }); console.log("emit updateItem first time", 'id: 5') updateItem$.next({ id: '5', name: 'Romain Geffrault 5', }); setTimeout(() => { console.log("emit updateItem second time", 'id: 4') updateItem$.next({ id: '4', name: 'Romain Geffrault 4, updated twice', }); }, 5000) Voici le résultat: Grâce à l'opérateur groupBy, c'est simple de lancer plusieurs requêtes API en parallèle. Voici le lien pour voir cette beauté en action. Dans l'exemple, j'ai groupé par id, qui un cas basique, mais on peut pousser plus loin la notion de grouper. Afficher une liste d'entités avec des statuts de chargement réactif sur Angular Je vais être pragmatique et te présenter une implémentation qui se rapproche d'un cas réel que tu pourras réutiliser facilement. Malheureusement, le lien stackblitz ne marche pas, mais voici le repo du code que tu peux cloner pour essayer. J'ai utilisé NodeJs v.20 npm i ng serve Les pages qui nous intéressent sont : src\app\features\data-list\data-list.component.ts & src\app\features\data-list\data-list.component.html J'ai ajouté pas mal de commentaires pour t'expliquer le fonctionnement de certaines fonctions RxJs si tu n'as pas l'habitude. J'ai utilisé ici une approche déclarative/réactive. Car c'est ma façon de faire avec ces nombreux avantages. Tu remarqueras que j'ai géré le cas où on laisse les appels API se terminer avant d'unsubscribe les stream (comme ça pas de memoryleak et pas

Mar 20, 2025 - 22:25
 0
Angular & RxJS : fine-grained CRUD status tracking sur une liste de données (thanks to groupBy

L'article montre comment appliquer une requête API à chaque entité d'une liste, afficher son statut de chargement et exécuter les requêtes en parallèle.

Tu trouveras un exemple que tu pourras utiliser dans tes applications.

Voici un exemple :

Une liste de données avec des items qui ont été modifiés et d'autres supprimé grâce à groupBy RxJs

Pour faire ça facilement, je vais te présenter l'opérateur groupBy que l'on va utiliser avec un autre opérateur custom que j'utilise fréquemment "statedStream".

Créer l'opérateur "statedStream" pour connaître le statut de chargement d'une requête

L'opérateur statedStream permet de connaître le statu de chargement d'une requête asynchrone, je l'utilise la plupart du temps lors d'un appel API.

Le fonctionnement est similaire à celui de httpRessource d'Angular, sauf que l'on reste dans le domaine des observables.

updateItem$
  .pipe(
    switchMap((updateItem) => // everytime, updateItem$ emit a new value, it cancels the existing api call, and create a new API
      statedStream(updateApiCall$(updateItem), updateItem)
    )
  )
  .subscribe((data) => console.log('data', data));

Au lieu d'attendre de recevoir une unique valeur lorsque l'appel API se termine, statedStream va émettre une première valeur en indiquant que la requête est en train de charger (isLoading: true).

Les logs de réponses du statedStream RxJs

Voici une partie du code de statedStream :

export function statedStream<T>(
  toCall: Observable<T>,
  initialValue: T
): Observable<SatedStreamResult<T>> {
  return toCall.pipe(
    map(
      (result) =>
        ({
          isLoading: false,
          isLoaded: true,
          hasError: false,
          error: undefined,
          result,
        } satisfies SatedStreamResult<T>)
    ),
    startWith({
      isLoading: true,
      isLoaded: false,
      hasError: false,
      error: undefined,
      result: initialValue,
    }),
    catchError((error) =>
      of({
        isLoading: false,
        isLoaded: false,
        hasError: true,
        error,
        result: initialValue,
      })
    )
  );
}

A noter que si tu n'as pas l'habitude de travailler avec des streams d'observables, si l'appel api retourne une erreur, ton stream s'arrête et n'écoutera plus les prochaines émissions de la source (ici updateItem$).

Grâce à la fonction statedStream, les erreurs sont "catch" et récupéré dans le résultat. Cela permet de ne pas rompre le stream lors d'une erreur API et de continuer à émettre de nouvelles requêtes.

Voici un lien stackblitz pour voir cette fonction en détail

Débloquer le potentiel titanesque de l'opérateur groupBy

Je ne sais pas si tu as déjà utilisé l'opérateur groupBy de RxJs ? Perso, quand j'ai lu la doc la première fois, j'ai pas compris. La dixième fois non plus... Mais grâce à cet exemple, j'ai compris. Depuis, je comprends ;D

Si tu souhaites lire la doc, n'hésite pas, il y a aussi un exemple sur stackblitz.

Grosso modo, on réutilise l'exemple de statedStream et on l'ajoute dans le stream du groupBy:

updateItem$
  .pipe(
    groupBy((updateItem) => updateItem.id), // create a group for each unique id
    mergeMap((group$) => {
      console.log('group$', group$.key);
      return group$.pipe(
        switchMap((updateItem) =>
          statedStream(updateApiCall$(updateItem), updateItem)
        )
      );
    })
  )
  .subscribe((data) => console.log('Received:', data));

Ensuite, on émet quelques update et là, tu vas comprendre. On émet 2 update successivement, puis un troisième update après 5s.

console.log("emit updateItem first time", 'id: 4')
updateItem$.next({
  id: '4',
  name: 'Romain Geffrault 4',
});

console.log("emit updateItem first time", 'id: 5')
updateItem$.next({
  id: '5',
  name: 'Romain Geffrault 5',
});


setTimeout(() => {
  console.log("emit updateItem second time", 'id: 4')
  updateItem$.next({
    id: '4',
    name: 'Romain Geffrault 4, updated twice',
  });
}, 5000)

Voici le résultat:

Logs de réponses du statedStream et groupBy RxJs

Grâce à l'opérateur groupBy, c'est simple de lancer plusieurs requêtes API en parallèle.

Voici le lien pour voir cette beauté en action.

Dans l'exemple, j'ai groupé par id, qui un cas basique, mais on peut pousser plus loin la notion de grouper.

Afficher une liste d'entités avec des statuts de chargement réactif sur Angular

Reactive Data List with Detailed Entity Status Tracking with groupBy RxJs Angular

Je vais être pragmatique et te présenter une implémentation qui se rapproche d'un cas réel que tu pourras réutiliser facilement.

Managing Entity Status in Reactive Data Lists with RxJS

Malheureusement, le lien stackblitz ne marche pas, mais voici le repo du code que tu peux cloner pour essayer.

J'ai utilisé NodeJs v.20

npm i
ng serve

Les pages qui nous intéressent sont :
src\app\features\data-list\data-list.component.ts
& src\app\features\data-list\data-list.component.html

J'ai ajouté pas mal de commentaires pour t'expliquer le fonctionnement de certaines fonctions RxJs si tu n'as pas l'habitude.

J'ai utilisé ici une approche déclarative/réactive. Car c'est ma façon de faire avec ces nombreux avantages.

Tu remarqueras que j'ai géré le cas où on laisse les appels API se terminer avant d'unsubscribe les stream (comme ça pas de memoryleak et pas de cas bizarre).

J'adore cet exemple, mais je trouve qu'il peut être encore amélioré.

Par exemple, je dois répéter plusieurs fois les types de données pour TS.
Même si ce n'est pas très compliqué à rajouter, je n'ai pas géré le cas de garder l'affichage de la liste existante lors de la navigation, je n'ai pas mis la possibilité d'ajouter facilement des sélecteurs...

Un autre point qui peut être un peu gênant, c'est que malgré tout ces bouts de codes prennent de l'espace, et nuisent un peu à la visibilité globale du composant.

Une solution peut être de découper les différents cas qui se trouvent dans le scan, en fonction, ce qui s'apparente à des reducers. Mais ça peut être un peu contraignant pour récupérer les bons types, là où typescript les devines (infer).

On peut aussi imaginer qu'on veuille appliquer une même action à plusieurs items à la fois (bulkEdit...).

J'ai pris en compte tous ces manques et bien d'autres encore, je suis en train de créé un petit outil expérimental pour le moment qui va me permettre d'implémenter ces mécanismes de façon déclarative.

Ca se rapproche d'un outil de server-state-management, comme pourrait le faire TanStackQuery. Ca demande encore de la réflexion, mais j'ai hâte de te présenter le résultat.

Si t'as des questions, n'hésite pas, ou si tu souhaites en discuter n'hésite pas à commenter, je me ferai un plaisir de te répondre de mon mieux.

Ps: Je n'ai pas utilisé les signal, car:

  1. Je vais utiliser ce genre de pattern sur des app qui ne sont pas encore dans les dernières versions d'Angular.
  2. Il suffit de faire un toSignal si besoin
  3. Surtout que les trigger des updates/deletes sont des événements et pas des états, ce que gère parfaitement les observables, mais pas les signal.