Découvrez les nouveautés d'Angular 20 en quelques minutes

Angular 20 arrive avec plusieurs nouveautés et stabilisation des API: Zoneless, les APIs resource() et httpResource(), un nouveau runner de tests, etc. La vidéo vous donne un aperçu de ces nouveautés.

Abonnez-vous à notre chaîne

Pour profiter des prochaines vidéos sur Angular, abonnez-vous à la nouvelle chaîne YouTube !

Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

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 de user$ | 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 émission
  • requireSync : Exige une émission synchrone (pour les BehaviorSubject)
  • injector : Injector à utiliser si appelé hors contexte d'injection
  • manualCleanup : Désactive le nettoyage automatique
  • equal : 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())
  )
);