# A la découverte de rxMethod

Depuis l'arrivée des Signals dans Angular, une question revient sans cesse dans les équipes : comment continuer à profiter de la puissance de RxJS, de ses opérateurs, de sa gestion fine des flux asynchrones, de son `switchMap` par exemple qui annule les requêtes obsolètes et le tout en adoptant un modèle d'état basé sur les signaux ?

C'est exactement le problème que résout `rxMethod`, une fonction fournie par le package `@ngrx/signals/rxjs-interop`.

Elle ne remplace pas RxJS, elle le fait cohabiter proprement avec les signaux.

## Le problème qu'elle résout

Avant `rxMethod`, connecter un flux RxJS à un signal demandait souvent du bricolage : un `effect()` qui souscrit manuellement à un Observable, une gestion artisanale du désabonnement, et le risque bien réel de fuites mémoire ou de traitements en double si l'on n'y prête pas attention.

*Voici un exemple représentatif de ce qu'on écrivait "avant" :*

```typescript
import { Component, effect, inject, signal, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

@Component({ /* ... */ })
export class TodoList implements OnDestroy {
  #todosService = inject(TodosService);
  #subscription?: Subscription;

  userId = signal(1);
  todos = signal<Todo[]>([]);

  constructor() {
    effect(() => {
      const id = this.userId(); // lecture du signal → dépendance déclarée

      // ⚠️ On souscrit manuellement à chaque exécution de l'effect
      this.#subscription = this.#todosService.getByUserId(id).subscribe({
        next: (todos) => this.todos.set(todos),
        error: (err) => console.error(err),
      });
    });
  }

  ngOnDestroy() {
    this.#subscription?.unsubscribe();
  }
}
```

Ce code a deux défauts typiques :

*   **fuite mémoire / traitements en double** : à chaque changement de `userId`, l'`effect` relance une souscription, mais n'annule jamais la précédente. Si l'utilisateur change rapidement d'identifiant, plusieurs requêtes HTTP restent actives en parallèle, et rien ne garantit que les réponses arrivent dans l'ordre. Ainsi, la plus ancienne peut très bien écraser la plus récente.
    
*   **nettoyage fragile** : on ne désabonne qu'à la destruction du composant (`ngOnDestroy`), pas entre deux exécutions de l'`effect`. Pour corriger ça "à la main", il faudrait gérer soi-même l'ancienne souscription via le `onCleanup` de l'`effect` :
    

```typescript
effect((onCleanup) => {
  const id = this.userId();

  const sub = this.#todosService.getByUserId(id).subscribe({
    next: (todos) => this.todos.set(todos),
  });

  onCleanup(() => sub.unsubscribe());
});
```

## rxResource, la réponse native d'Angular à ce problème

Face à ce genre de bricolage, Angular propose désormais sa propre solution native pour la lecture de données asynchrones : `rxResource`, dans `@angular/core/rxjs-interop`.

Elle résout exactement le problème illustré ci-dessus, sans `effect()`, sans `Subscription` manuelle, sans `onCleanup` :

```typescript
import { Component, signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';

@Component({ /* ... */ })
export class TodoList {
  #todosService = inject(TodosService);
  userId = signal(1);

  todosResource = rxResource({
    params: () => ({ userId: this.userId() }),
    stream: ({ params }) => this.#todosService.getByUserId(params.userId),
  });
}
```

`rxResource` gère lui-même l'état complet de la requête :

*   valeur : `todosResource.value()` ;
    
*   chargement : `todosResource.isLoading()` ;
    
*   erreur : `todosResource.error()` ;
    
*   l'annulation automatique de la requête précédente à chaque changement de `params` sans qu'on ait à écrire le moindre `switchMap` ni à se soucier du désabonnement.
    

Sur ce cas d'usage précis, une lecture pilotée par un paramètre réactif est bien évidemment plus simple qu'un `rxMethod`.

> Alors, `rxResource` rend-il `rxMethod` inutile ?  
> 
> Pas tout à fait, et c'est ce que la suite de cet article va démontrer.

## Anatomie de rxMethod

```typescript
import { rxMethod } from '@ngrx/signals/rxjs-interop';

function rxMethod<Input>(
  generator: (source$: Observable<Input>) => Observable<unknown>,
  config?: { injector?: Injector }
): RxMethod<Input>
```

`rxMethod` prend en paramètre un générateur qui n'est rien d'autre qu'une fonction qui reçoit un flux (`Observable<Input>`) et retourne un `Observable` transformé, typiquement via un `pipe()`.

Le paramètre de type `Input` fixe le type des données attendues en entrée, indépendamment de la forme sous laquelle l'appelant les fournit.

```typescript
import { Component, inject, signal } from '@angular/core';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { switchMap } from 'rxjs';
import { tapResponse } from '@ngrx/operators';

@Component({ /* ... */ })
export class TodoList {
  readonly #todosService = inject(TodosService);
  readonly userId = signal(1);
  readonly todos = signal<Todo[]>([]);

  readonly loadTodos = rxMethod<number>(
    switchMap((id) =>
      this.#todosService.getByUserId(id).pipe(
        tapResponse({
          next: (todos) => this.todos.set(todos),
          error: console.error,
        })
      )
    )
  );

  constructor() {
    this.loadTodos(this.userId);   // un Signal<number>
    this.loadTodos(this.userId$);  // un Observable<number>
    this.loadTodos(1);            // une valeur brute, number
  }
}
```

Trois façons d'appeler `loadTodos` sont possibles :

*   si c'est un **Signal**, il crée un `effect()` interne qui pousse la valeur courante du signal dans le flux source à chaque changement ;
    
*   si c'est un **Observable**, il s'y abonne directement et transmet chaque valeur émise dans le flux source ;
    
*   si c'est une **valeur statique**, il l'émet une seule fois dans le flux source, immédiatement.
    

La logique métier ne change pas, seule la source d'entrée varie. Dans les trois cas, la valeur finit par arriver, sous forme de `number`.

## Pourquoi le choix de l'opérateur de flattening compte

`rxMethod` ne dicte aucun opérateur d'aplatissement particulier : c'est à vous de choisir entre `switchMap`, `mergeMap`, `concatMap` ou `exhaustMap` selon le comportement de concurrence souhaité.

*   `switchMap` : annule la requête précédente dès qu'une nouvelle arrive. C'est idéal pour une recherche ou un chargement de détail par identifiant.
    
*   `concatMap` : traite les requêtes dans l'ordre, une par une. Il est utile pour des écritures séquentielles.
    
*   `mergeMap` : lance tout en parallèle pour des opérations indépendantes.
    
*   `exhaustMap` : ignore les nouvelles requêtes tant que la précédente n'est pas terminée. C'est très pratique pour éviter les doubles soumissions.
    

> Ce choix a un impact direct sur la fiabilité de l'application, notamment pour éviter les fameuses "race conditions" où une réponse arrivée en retard écrase un résultat plus récent.

## Le contexte d'injection

`rxMethod` doit, par défaut, être appelée dans un contexte d'injection Angular (constructeur, champ de classe, ou tout endroit où `inject()` est disponible). Si ce n'est pas possible, on peut fournir explicitement un `Injector` :

```typescript
constructor() {
  this.loadTodos = rxMethod<number>(
    switchMap((id) => /* ... */),
    { injector: this.injector }
  );
}
```

Cette contrainte garantit que le cycle de vie de la souscription est correctement rattaché, et donc automatiquement nettoyé à la destruction du composant.

Plus besoin de gérer manuellement un `Subscription` et son `unsubscribe()`.

## Les mutations : là où rxResource montre ses limites

`rxResource` a un modèle mental très clair : une fonction `stream` qui se relance automatiquement chaque fois que `params()` produit une nouvelle valeur.

C'est un peu comme une requête qu'on rejoue à chaque changement de paramètre. C'est excellent pour du `GET`.

Cependant, voyons ce qui se passe si on essaie de le détourner pour une mutation.

```typescript
// ⚠️ Détournement de rxResource pour une sauvegarde - à éviter
export class TodoEditor {
  #todosService = inject(TodosService);
  todoToSave = signal<Todo | null>(null);

  saveResource = rxResource({
    params: () => this.todoToSave(),
    stream: ({ params }) => {
      if (!params) return of(undefined);
      return this.#todosService.save(params);
    },
  });

  onSubmit(todo: Todo) {
    this.todoToSave.set(todo); // 👈 on force la mutation via un changement de "requête"
  }
}
```

Trois problèmes concrets apparaissent immédiatement :

*   **la déduplication casse le renvoi :** `rxResource` ne relance `stream` que lorsque `params()` change de valeur. Si l'utilisateur soumet deux fois le même formulaire (même objet `todo`), le second clic **ne déclenche rien du tout** : `params()` n'a pas changé, donc la mutation n'est jamais rejouée. C'est un comportement correct pour une lecture, mais un bug silencieux pour une écriture.
    
*   **l'annulation implicite est dangereuse :** si `todoToSave()` change une nouvelle fois pendant qu'une sauvegarde précédente est encore en cours, le comportement de type `switchMap` de `rxResource` annule la requête HTTP en cours. Pour une lecture, perdre une réponse obsolète est sans conséquence. Pour une écriture, cela peut vouloir dire qu'une sauvegarde en base de données est interrompue au milieu de son exécution.
    
*   **aucun contrôle sur la concurrence : i**mpossible de dire à `rxResource` "ignore les nouveaux appels tant que le précédent n'est pas terminé" (`exhaustMap`) ou "empile-les dans l'ordre" (`concatMap`). Le modèle est figé sur un comportement proche de `switchMap`, pensé pour représenter un état, pas une suite d'événements.
    

Voyons maintenant la même mutation avec `rxMethod` :

```typescript
export class TodoEditor {
  #todosService = inject(TodosService);
  saving = signal(false);

  saveTodo = rxMethod<Todo>(
    exhaustMap((todo) => {
      this.saving.set(true);
      return this.#todosService.save(todo).pipe(
        tapResponse({
          next: () => this.saving.set(false),
          error: (err) => {
            console.error(err);
            this.saving.set(false);
          },
        })
      );
    })
  );

  onSubmit(todo: Todo) {
    this.saveTodo(todo); // 👈 chaque appel est un événement, pas une valeur d'état
  }
}
```

Ici, tout fonctionne comme attendu :

*   **chaque appel à** `saveTodo(todo)` **déclenche systématiquement un envoi**, même avec un `todo` identique au précédent. `rxMethod` alimente en interne un flux d'événements (proche d'un `Subject`) : il ne compare pas les valeurs, il les pousse. Rien n'est dédupliqué.
    
*   `exhaustMap` **protège explicitement contre le double clic** : tant qu'une sauvegarde est en cours, les appels suivants sont ignorés. Un choix assumé, contrairement au comportement implicite et non paramétrable de `rxResource`.
    
*   **aucune sauvegarde en cours n'est jamais annulée involontairement**, puisque rien ne force `rxMethod` à réagir à un changement de "paramètre" : c'est l'appel explicite qui déclenche l'action.
    

> La différence de fond tient en une phrase : `rxResource` **modélise un état** (une donnée dérivée d'un paramètre, qu'on relit), `rxMethod` **modélise un flux d'événements** (une action qu'on déclenche).
> 
>   
> Vouloir faire des mutations avec le premier revient à forcer un modèle déclaratif à se comporter comme un modèle impératif. Ça fonctionne parfois en apparence, mais les cas limites (double soumission, annulation, ordre d'exécution) finissent toujours par se rappeler à vous.

Une bonne règle de partage des rôles :

| Besoin | Outil recommandé |
| --- | --- |
| Charger une donnée en fonction d'un signal (recherche, détail par id, pagination) | `rxResource` |
| Déclencher une action ponctuelle (sauvegarde, suppression, envoi de formulaire) | `rxMethod` |
| Chaîner des opérateurs RxJS complexes sur un flux d'événements arbitraire | `rxMethod` |

Les deux ne s'excluent pas : il est courant de voir `rxResource` pour l'affichage et `rxMethod` pour les mutations coexister dans le même composant.

## Au-delà des cas de lecture

Rien n'empêche non plus d'utiliser `rxMethod` pour des appels HTTP simples, sans structure d'état complexe autour :

```typescript
export class UserListComponent {
  #http = inject(HttpClient);
  users = signal<User[]>([]);

  loadUsers = rxMethod<void>(() =>
    this.#http.get<User[]>('/api/users').pipe(
      tap((data) => this.users.set(data))
    )
  );
}
```

C'est une alternative intéressante aux appels RxJS "à la main" dans un `effect()`, avec une API plus explicite sur ce qui déclenche réellement l'effet de bord.

En somme, `rxMethod` n'est pas un remplaçant de RxJS, ni un concurrent des signaux. Il joue un rôle de pont. Il permet de :

*   **conserver** la richesse des opérateurs RxJS (debounce, annulation, combinaison de flux) ;
    
*   **s'intégrer** nativement aux signaux Angular, sans code de liaison superflu ;
    
*   **automatiser** la gestion du cycle de vie des souscriptions ;
    
*   **accepter indifféremment** une valeur, un signal ou un observable en entrée ;
    
*   **couvrir les mutations** là où `rxResource`, pensé pour la lecture, montre ses limites.
    

Pour toute équipe qui migre progressivement vers les signaux tout en gardant des flux de données complexes basés sur RxJS, `rxMethod` constitue aujourd'hui l'un des outils les plus pragmatiques de l'écosystème NgRx, particulièrement complémentaire de `rxResource` pour tout ce qui touche à l'écriture plutôt qu'à la lecture.

> *Pour plus d'informations 👉🏽* [rxMethod](https://ngrx.io/guide/signals/rxjs-integration)
