Dans cette deuxième partie de la série, nous allons voir comment migrer l’un des schémas les plus courants dans les applications Angular : un service qui stocke son état dans un BehaviorSubject.
Il n’est pas obligatoire de migrer ce schéma, car les BehaviorSubjects fonctionnent parfaitement. Toutefois, passer aux signals peut être un excellent moyen de simplifier votre base de code, de supprimer le code répétitif lié aux observables (comme les async pipes et les blocs subscribe) et de rendre la gestion de l’état plus déclarative.
Avant : partager des données dans un service avec RxJs
Imaginez que vous devez récupérer des produits depuis votre API, les afficher dans un composant de liste et les rendre accessibles à d’autres composants. Il nous faut donc une forme d’état partagé pour les produits. Cela peut se faire avec le simple schéma « Subject dans un service », comme illustré ci-dessous :
Service des produits
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly http = inject(HttpClient);
private readonly products = new BehaviorSubject<Product[]>([]); // BehaviorSubject sur lequel nous pouvons appeler .next()
public readonly products$ = this.products.asObservable(); // valeur exposée publiquement, en lecture seule
public loadProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products').pipe(
tap((products) => {
this.products.next(products); //
})
);
}
}Dans le composant, nous pouvons ensuite récupérer les données et utiliser le flux observable exposé par le service. Pour obtenir sa valeur, nous pouvons utiliser AsyncPipe, qui s’abonne au flux.
Une autre stratégie consiste à s’abonner manuellement avec la méthode .subscribe(). J’évite autant que possible cette approche afin de garder un code plus déclaratif et d’éviter les éventuelles fuites mémoire si l’on oublie de se désabonner, mais c’est un autre sujet.
Composant de liste des produits
@Component({
selector: 'app-product-list',
template: `
@for(product of products$ | async) {
<div>
{{ product.name }}
</div>
}
`,
imports: [AsyncPipe] // Il faut importer async pipe pour s'abonner dans le template
})
export class ProductListComponent {
private readonly productService = inject(ProductService);
public readonly products$ = this.productService.products$;
ngOnInit() {
this.productService.loadProducts(); // récupérer les produits à l'initialisation
}
}Maintenant : partager des données avec les signals
Avec les signals, nous pouvons conserver exactement la même idée : stocker la liste des produits une seule fois dans le service et permettre à plusieurs composants de l’utiliser, mais avec moins de code répétitif.
Plus de BehaviorSubject, plus de asObservable() et plus besoin d’AsyncPipe pour extraire la valeur.
Les signals offrent un moyen simple et direct de lire et de mettre à jour l’état.
Réécrivons le ProductService en utilisant un signal à la place d’un BehaviorSubject :
Service des produits
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly http = inject(HttpClient);
private readonly _products = signal<Product[]>([]); // signal modifiable privé
public readonly products = this._products.asReadonly(); // exposer un signal en lecture seule au reste de l'application
public loadProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products').pipe(
tap((products) => {
this.products.set(products);
})
);
}
}Dans le composant, la consommation des données devient plus simple. Nous n’avons pas besoin d’AsyncPipe, car un signal se lit comme un simple getter :
Composant de liste des produits
@Component({
selector: 'app-product-list',
template: `
@for(product of products()) {
<div>
{{ product.name }}
</div>
}
`,
imports: [] // AsyncPipe n'est pas nécessaire
})
export class ProductListComponent {
private readonly productService = inject(ProductService);
public readonly products = this.productService.products; // utiliser directement le signal
ngOnInit() {
this.productService.loadProducts();
}
}L’idée générale reste la même : le service récupère et stocke les données, et n’importe quel composant peut les lire. Mais les signals suppriment plusieurs couches d’API propres aux Observables, ce qui rend le code un peu plus facile à suivre.
Conclusion
Migrer un service d’état partagé d’un BehaviorSubject vers un signal n’est pas obligatoire, mais c’est un gain facile si vous souhaitez réduire le code répétitif et améliorer la lisibilité.
Vous conservez la même idée (une seule source de vérité dans le service), avec moins de concepts à comprendre : plus de asObservable(), plus d’async pipe, et un accès plus direct aux données dans vos composants. C’est un petit changement, mais il rend souvent le flux des données plus facile à suivre.
Dans la prochaine partie de cette série, nous aborderons une question plus vaste : dans quels cas faut-il conserver RxJS et les Observables plutôt que passer aux signals ?
Car même si les signals sont excellents pour l’état, RxJS reste très adapté à de nombreux autres scénarios. Savoir quand utiliser chacun de ces outils contribue à une architecture vraiment solide.