~2 min de lecture
Le double subscribe, c’est un grand classique chez les devs Angular.
Souvent invisible, toujours piégeux.
Voici 5 façons progressives de faire mieux, en partant du pire pour finir par le plus moderne.
❌ 1) Le mauvais exemple — double subscribe (avec inject)
Une version fonctionnelle, mais dangereuse à long terme.
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { ReplaySubject } from 'rxjs';
type User = { id: string; name: string };
@Component({
selector: 'user-page-bad',
template: `
@if (user) {
<p>{{ user.name }}</p>
} @else {
<p>Chargement…</p>
}
`,
})
export class UserPageBad {
private readonly route = inject(ActivatedRoute);
private readonly http = inject(HttpClient);
protected readonly user: User | null = null;
ngOnInit() {
// Premier subscribe : écoute de la route
this.route.paramMap.subscribe(params => {
const id = params.get('id');
if (!id) return;
// Second subscribe imbriqué : appel HTTP (anti-pattern)
this.http.get<User>(`/api/users/${id}`).subscribe(user => {
this.user = user;
});
});
}
}
Problèmes :
- Souscriptions imbriquées → pyramide d’enfer.
- Nettoyage compliqué (
unsubscribeobligatoire). - Risque de requêtes multiples ou de fuites mémoire.
- Pas facilement compréhensible.
✅ 2) Version RxJS propre — la baseline switchMap
L’équivalent “clean” et réactif avec un flux plat et auto-nettoyé.
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { map, switchMap } from 'rxjs/operators';
type User = { id: string; name: string };
@Component({
selector: 'user-page-rx',
template: `
@if (user$ | async; as user) {
<p>{{ user.name }}</p>
} @else {
<p>Chargement…</p>
}
`,
})
export class UserPageRx {
private readonly route = inject(ActivatedRoute);
private readonly http = inject(HttpClient);
protected readonly user$ = this.route.paramMap.pipe(
map(params => params.get('id')!),
switchMap(id => this.http.get<User>(`/api/users/${id}`))
);
}
Pourquoi c’est mieux :
- Zéro
subscribe()manuel (| asyncgère tout). switchMapannule la requête précédente si l’ID change.- Code lisible, testable, sans effet de bord.
⚡ 3) Version moderne Signals — rxResource()
À partir d’Angular 19, on peut combiner RxJS et Signals via rxResource().
C’est la version la plus naturelle pour du HttpClient.
import { Component, inject, rxResource } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
type User = { id: string; name: string };
@Component({
selector: 'user-page-rx-resource',
template: `
@if (userRes.isLoading()) {
<p>Chargement…</p>
} @else if (userRes.error(); as e) {
<p>Oups : {{ e.message }}</p>
} @else if (userRes.value(); as user) {
<p>{{ user.name }}</p>
}
`,
})
export class UserPageRxResource {
private readonly route = inject(ActivatedRoute);
private readonly http = inject(HttpClient);
// Signal issu de la route
private readonly id = toSignal(
this.route.paramMap.pipe(map(p => p.get('id')!)),
{ initialValue: undefined as string | undefined }
);
protected readonly userRes = rxResource({
params: () => this.id(),
stream: ({ params }) =>
// ne déclenche pas si undefined
params ? this.http.get<User>(`/api/users/${params}`) : undefined,
});
}
Atouts pédagogiques :
- Liaison directe entre RxJS et Signals.
- Annulation automatique via
AbortSignal. isLoading(),error(),value()intégrés.- Pas de
subscribe()niasyncnécessaire.
💡 4) Version Input + Resource — composant réutilisable
Découple le composant des routes, parfait avec withComponentInputBinding().
import { Component, input, inject, rxResource } from '@angular/core';
import { HttpClient } from '@angular/common/http';
type User = { id: string; name: string };
@Component({
selector: 'user-by-input',
template: `
@if (userRes.isLoading()) {
<p>Chargement…</p>
} @else if (userRes.error(); as e) {
<p>Oups : {{ e.message }}</p>
} @else if (userRes.value(); as user) {
<p>{{ user.name }}</p>
}
`,
})
export class UserByInput {
private readonly http = inject(HttpClient);
// Id fourni par un parent ou une route (data, resolver, params)
private readonly id = input.required<string>();
protected readonly userRes = rxResource({
params: () => this.id(),
stream: ({ params }) =>
this.http.get<User>(`/api/users/${params}`),
});
}
Pourquoi c’est top :
- Complètement découplé du router.
- Réutilisable partout.
- Gestion intégrée des états et annulation.
- Compatible
withComponentInputBinding().
🚀 5) Version ultra-native — httpResource()
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';
import { toSignal } from '@angular/core/rxjs-interop';
import { httpResource } from '@angular/common/http';
type User = { id: string; name: string };
@Component({
selector: 'user-page-http-resource',
template: `
@if (userRes.isLoading()) {
<p>Chargement…</p>
} @else if (userRes.error(); as e) {
<p>Oups : {{ e.message }}</p>
} @else if (userRes.value(); as user) {
<p>{{ user.name }}</p>
}
`,
})
export class UserPageHttpResource {
private readonly route = inject(ActivatedRoute);
private readonly id = toSignal(
this.route.paramMap.pipe(map(p => p.get('id')!)),
{ initialValue: undefined as string | undefined }
);
protected readonly userRes = httpResource<User>(() => {
const id = this.id();
// ne déclenche pas si undefined
return id ? `/api/users/${id}` : undefined;
});
}
Pourquoi c’est cool :
- Aucun
inject(HttpClient)nécessaire. AbortSignal+ SSR cache intégrés.- API concise :
httpResource<User>(() => url). - Idéal pour le code “data-first” avec SSR.
🧩 Résumé pédagogique — tableau comparatif
- Tu es 100% RxJS aujourd’hui ➜
switchMap(baseline). - Tu veux RxJS et un état UI “clé en main” ➜
rxResource(). - Tu veux un composant réutilisable (découplé du Router) ➜ Input +
rxResource(). - Tu vises simplicité HTTP ➜
httpResource(). - Évite toujours ➜ double subscribe.
No-trigger tip (important) :
UtiliseinitialValue: undefineddanstoSignal(...)et fais retournerundefinedparrequest/factory tant que l’ID n’est pas connu. Ainsi, aucune requête parasite n’est lancée.
Envie de découvrir EasyAngularKit et son programme ?
→ Découvre EasyAngularKit : https://pim.ms/C31g7p2