Appearance
toSignal() : Convertir un Observable en Signal
Imaginez que vous ayez un robinet d'eau (Observable) qui coule en continu, et que vous vouliez stocker cette eau dans un réservoir (Signal) pour pouvoir l'utiliser quand vous le souhaitez. C'est exactement ce que fait toSignal()
!
Cette fonction magique d'Angular vous permet de transformer vos Observables RxJS en Signals, vous donnant ainsi accès à la dernière valeur émise de manière synchrone et réactive, sans avoir à gérer les abonnements manuellement.
Qu'est-ce que toSignal() ?
toSignal()
est une fonction utilitaire qui convertit un Observable RxJS en Signal Angular. Elle s'abonne automatiquement à l'Observable et expose la dernière valeur émise sous forme de Signal.
typescript
import { toSignal } from '@angular/core/rxjs-interop';
// Observable qui émet des valeurs toutes les secondes
const time$ = interval(1000);
// Conversion en Signal
const time = toSignal(time$);
Pourquoi utiliser toSignal() ?
Avantages principaux
- Lecture synchrone : Plus besoin d'utiliser l'async pipe dans les templates
- Gestion automatique des abonnements : Le nettoyage se fait automatiquement
- Performance améliorée : Les Signals sont plus performants que les Observables pour l'affichage
- Syntaxe plus simple :
user()
au lieu deuser$ | async
Cas d'usage typiques
- Afficher des données HTTP dans vos composants
- Composer l'état de l'interface avec
computed()
- Réagir aux changements de signaux pour déclencher des requêtes
API de base
La fonction toSignal()
accepte un Observable et des options optionnelles :
typescript
toSignal<T>(source$, options?)
Options principales
initialValue
: Valeur par défaut avant la première émissionrequireSync
: Exige une émission synchrone (pour les BehaviorSubject)injector
: Injector à utiliser si appelé hors contexte d'injectionmanualCleanup
: Désactive le nettoyage automatiqueequal
: Fonction de comparaison personnalisée
Exemples pratiques
1. Données HTTP avec gestion d'erreurs
Voici comment récupérer des utilisateurs depuis une API :
typescript
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
import { catchError, of } from 'rxjs';
@Component({
selector: 'app-users',
template: `
@if (users()) {
@for (user of users(); track user.id) {
<div>{{ user.name }}</div>
}
}
`
})
export class UsersComponent {
private http = inject(HttpClient);
// Conversion de l'Observable HTTP en Signal
// avec une valeur initiale vide et gestion d'erreur
users = toSignal(
this.http.get<User[]>('/api/users').pipe(
catchError(() => of([])) // En cas d'erreur, retourne un tableau vide
),
{ initialValue: [] } // Valeur par défaut avant la réponse
);
}
Bonne pratique
Toujours utiliser catchError
dans le pipe pour éviter que le Signal ne jette une erreur lors de la lecture.
2. Source "chaude" avec BehaviorSubject
Pour les sources qui émettent immédiatement une valeur :
typescript
import { Component } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-filter',
template: `
<input [value]="filter()" (input)="updateFilter($event)" />
<p>Filtre actuel : {{ filter() }}</p>
`
})
export class FilterComponent {
// BehaviorSubject qui émet immédiatement une valeur
private readonly _filter$ = new BehaviorSubject<string>('');
// Conversion avec requireSync pour éviter undefined
filter = toSignal(this._filter$, { requireSync: true });
updateFilter(event: Event) {
const value = (event.target as HTMLInputElement).value;
this._filter$.next(value);
}
}
Attention
requireSync: true
ne fonctionne qu'avec les sources qui émettent immédiatement (BehaviorSubject, ReplaySubject, of()). Sinon, vous obtiendrez une erreur NG0601.
3. Recherche réactive avec debounce
Créons un système de recherche qui réagit aux changements d'un signal :
typescript
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap, catchError, of } from 'rxjs';
@Component({
selector: 'app-search',
template: `
<input [value]="query()" (input)="query.set($event.target.value)" />
@if (results()) {
@for (result of results(); track result.id) {
<div>{{ result.title }}</div>
}
}
`
})
export class SearchComponent {
private http = inject(HttpClient);
// Signal pour la requête de recherche
query = signal('');
// Conversion de l'Observable de recherche en Signal
results = toSignal(
toObservable(this.query).pipe(
debounceTime(300), // Attendre 300ms après la dernière frappe
distinctUntilChanged(), // Éviter les requêtes identiques
switchMap(q =>
this.http.get<SearchResult[]>('/api/search', { params: { q } })
),
catchError(() => of([]))
),
{ initialValue: [] }
);
}
Gestion du cycle de vie
Par défaut, toSignal()
se désabonne automatiquement quand le composant est détruit. Pas besoin de gérer takeUntil
!
typescript
@Component({
selector: 'app-auto-cleanup',
template: `<div>{{ data() }}</div>`
})
export class AutoCleanupComponent {
private http = inject(HttpClient);
// L'abonnement sera automatiquement nettoyé à la destruction du composant
data = toSignal(
this.http.get<string>('/api/data')
);
}
Si vous voulez garder l'abonnement actif jusqu'au complete
de l'Observable :
typescript
// L'abonnement durera jusqu'au complete de l'Observable
data = toSignal(
this.http.get<string>('/api/data'),
{ manualCleanup: true }
);
Bonnes pratiques
1. Une seule conversion par source
Évitez de recréer le signal à chaque lecture :
typescript
// ❌ Mauvais : recréation à chaque appel
getUsers() {
return toSignal(this.http.get<User[]>('/api/users'));
}
// ✅ Bon : une seule conversion
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] }
);
2. Normaliser l'état
Exposez des signaux séparés pour les données, le chargement et les erreurs :
typescript
@Component({
selector: 'app-user-list',
template: `
@if (isLoading()) {
<p>Chargement...</p>
} @else if (error()) {
<p>Erreur : {{ error() }}</p>
} @else {
@for (user of users(); track user.id) {
<div>{{ user.name }}</div>
}
}
`
})
export class UserListComponent {
private http = inject(HttpClient);
// Signal pour les données
users = toSignal(
this.http.get<User[]>('/api/users').pipe(
catchError(() => of([]))
),
{ initialValue: [] }
);
// Signal pour l'état de chargement
isLoading = toSignal(
this.http.get<User[]>('/api/users').pipe(
map(() => false),
startWith(true),
catchError(() => of(false))
),
{ initialValue: true }
);
// Signal pour les erreurs
error = toSignal(
this.http.get<User[]>('/api/users').pipe(
map(() => null),
catchError(err => of(err.message))
),
{ initialValue: null }
);
}
3. Utiliser les bonnes options
typescript
// Pour les sources "chaudes" (BehaviorSubject, etc.)
hotData = toSignal(this.behaviorSubject$, { requireSync: true });
// Pour les sources "froides" (HTTP, etc.)
coldData = toSignal(this.http.get('/api/data'), { initialValue: [] });
Anti-patterns à éviter
1. Appeler toSignal() dans une boucle
typescript
// ❌ Mauvais : abonnements multiples
@for (id of ids(); track id) {
<div>{{ toSignal(this.getUser(id))() }}</div>
}
// ✅ Bon : utiliser computed()
userSignals = computed(() =>
this.ids().map(id => toSignal(this.getUser(id)))
);
2. Créer des signaux redondants
typescript
// ❌ Mauvais : signaux multiples pour la même source
users = toSignal(this.http.get('/api/users'));
userCount = toSignal(this.http.get('/api/users').pipe(map(users => users.length)));
// ✅ Bon : utiliser computed()
users = toSignal(this.http.get('/api/users'), { initialValue: [] });
userCount = computed(() => this.users().length);
Exemple complet : Store d'utilisateurs
Voici un exemple complet d'un service qui utilise toSignal()
pour gérer un store d'utilisateurs :
typescript
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, switchMap, catchError, of, startWith } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UsersStore {
private http = inject(HttpClient);
// Signal pour la requête de recherche
private readonly _query = signal('');
readonly query = this._query.asReadonly();
// Signal pour les utilisateurs filtrés
readonly users = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
switchMap(q =>
this.http.get<User[]>('/api/users', { params: { q } })
),
startWith([] as User[]),
catchError(() => of([]))
),
{ initialValue: [] }
);
// Signal pour le nombre d'utilisateurs
readonly userCount = computed(() => this.users().length);
// Méthode pour mettre à jour la requête
setQuery(query: string) {
this._query.set(query);
}
}
Et l'utilisation dans un composant :
typescript
@Component({
selector: 'app-users',
template: `
<input
[value]="usersStore.query()"
(input)="usersStore.setQuery($event.target.value)"
placeholder="Rechercher des utilisateurs..."
/>
<p>{{ usersStore.userCount() }} utilisateur(s) trouvé(s)</p>
@for (user of usersStore.users(); track user.id) {
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
}
`
})
export class UsersComponent {
usersStore = inject(UsersStore);
}
FAQ
Questions de base
Q: toSignal()
déclenche ma requête HTTP immédiatement, est-ce normal ? R: Oui, c'est normal ! toSignal()
s'abonne immédiatement à l'Observable. Pour les sources "froides" comme http.get()
, l'abonnement déclenche la requête.
Explication simple : Quand vous appelez toSignal(http.get('/api/users'))
, Angular dit "je veux écouter cette requête HTTP" et la requête part immédiatement. C'est comme si vous ouvriez un robinet - l'eau coule tout de suite !
Q: Comment éviter les valeurs undefined
? R: Utilisez initialValue
pour les sources "froides" ou requireSync: true
pour les sources "chaudes".
Exemple concret :
typescript
// ❌ Peut retourner undefined au début
users = toSignal(this.http.get('/api/users'));
// ✅ Avec une valeur par défaut
users = toSignal(
this.http.get('/api/users'),
{ initialValue: [] } // Tableau vide en attendant
);
// ✅ Avec une source "chaude" (BehaviorSubject)
filter = toSignal(
new BehaviorSubject(''),
{ requireSync: true } // Jamais undefined
);
Q: C'est quoi une source "chaude" vs "froide" ? R:
- Source "chaude" : Émet une valeur immédiatement quand on s'abonne (BehaviorSubject, ReplaySubject,
of()
) - Source "froide" : N'émet qu'après avoir reçu une demande (http.get(), interval(), fromEvent())
Exemple :
typescript
// Source "chaude" - émet immédiatement
const hot$ = new BehaviorSubject('valeur initiale');
const hotSignal = toSignal(hot$, { requireSync: true }); // ✅ OK
// Source "froide" - n'émet qu'après abonnement
const cold$ = this.http.get('/api/data');
const coldSignal = toSignal(cold$, { initialValue: [] }); // ✅ OK
Questions pratiques
Q: Je suis hors contexte d'injection, que faire ? R: Passez l'injector
dans les options : { injector: this.injector }
.
Exemple :
typescript
// Dans un service ou utilitaire
export class MyService {
private injector = inject(Injector);
private http = inject(HttpClient);
createSignal() {
return toSignal(
this.http.get('/api/data'),
{ injector: this.injector } // Fournir l'injector
);
}
}
Q: Quand utiliser toSignal()
vs garder l'Observable ? R:
- Utilisez
toSignal()
pour l'affichage et l'état local dans les composants - Gardez l'Observable pour les streams infinis (WebSocket, events) et les compositions complexes
Exemple concret :
typescript
// ✅ Bon : Signal pour l'affichage
@Component({
template: `
@for (user of users(); track user.id) {
<div>{{ user.name }}</div>
}
`
})
export class UserListComponent {
users = toSignal(this.userService.getUsers());
}
// ✅ Bon : Observable pour les événements continus
@Component({
template: `
<div>Messages reçus : {{ messageCount() }}</div>
`
})
export class ChatComponent {
// Pour les WebSockets, garder l'Observable
messages$ = this.chatService.getMessages();
// Mais convertir en Signal pour l'affichage
messageCount = toSignal(
this.messages$.pipe(map(messages => messages.length))
);
}
Questions de performance
Q: toSignal()
impacte-t-il les performances ? R: Non, bien au contraire ! Les Signals sont plus performants que les Observables pour l'affichage car ils ne déclenchent que les changements nécessaires.
Exemple de performance :
typescript
// ❌ Moins performant : async pipe recrée la vue à chaque émission
template: `
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
`
// ✅ Plus performant : Signal ne met à jour que ce qui change
template: `
@for (user of users(); track user.id) {
<div>{{ user.name }}</div>
}
`
Q: Comment éviter les abonnements multiples ? R: Ne créez qu'un seul signal par source Observable.
typescript
// ❌ Mauvais : abonnements multiples
@for (id of ids(); track id) {
<div>{{ toSignal(this.getUser(id))() }}</div>
}
// ✅ Bon : un seul abonnement partagé
users = toSignal(this.userService.getAllUsers());
Questions de débogage
Q: Mon signal ne se met pas à jour, pourquoi ? R: Vérifiez que votre Observable émet bien de nouvelles valeurs et que vous n'avez pas d'erreur non gérée.
typescript
// ✅ Bon : avec gestion d'erreur
users = toSignal(
this.http.get('/api/users').pipe(
catchError(err => {
console.error('Erreur HTTP:', err);
return of([]); // Valeur de repli
})
),
{ initialValue: [] }
);
Q: Comment déboguer un signal ? R: Utilisez effect()
pour observer les changements ou console.log dans le template.
typescript
// Observer les changements du signal
effect(() => {
console.log('Users mis à jour:', this.users());
});
// Ou dans le template
template: `
<div>{{ users() | json }}</div>
`
Questions avancées
Q: Puis-je utiliser toSignal()
avec des Observables qui ne se terminent jamais ? R: Oui, mais attention au cycle de vie ! Par défaut, toSignal()
se désabonne quand le composant est détruit.
typescript
// ✅ Bon : nettoyage automatique
timer = toSignal(interval(1000)); // Se désabonne automatiquement
// ✅ Bon : garder l'abonnement actif
timer = toSignal(
interval(1000),
{ manualCleanup: true } // Reste actif jusqu'au complete
);
Q: Comment combiner plusieurs signaux ? R: Utilisez computed()
pour combiner des signaux, pas toSignal()
.
typescript
// ✅ Bon : combiner des signaux
users = toSignal(this.userService.getUsers());
filter = signal('');
filteredUsers = computed(() =>
this.users().filter(user =>
user.name.includes(this.filter())
)
);