Replay

Apprendre Angular en 1h

Je me donne un objectif: vous faire découvrir et apprendre Angular en 1 heure: composant, syntaxe dans les templates, les directives, les signaux, les routeurs, les services, l'injection de dépendances, les observables et les requêtes HTTP. Le nécessaire pour faire une application Angular !.

Skip to content

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

Lecture de l'état dans NgRx Signal Store

La lecture de l'état est une fonctionnalité fondamentale de toute bibliothèque de gestion d'état. NgRx Signal Store offre une approche particulièrement intuitive grâce à l'utilisation des signaux d'Angular.

La lecture d'état dans la vie quotidienne

Pour comprendre la lecture d'état, imaginez un tableau de bord dans une voiture. Ce tableau affiche différentes informations comme la vitesse, le niveau de carburant, ou la température du moteur. Ces affichages ne modifient pas l'état de la voiture, ils se contentent de lire et présenter les données actuelles. De la même façon, les mécanismes de lecture dans Signal Store permettent d'accéder aux données de l'état sans le modifier.

Accès direct aux propriétés du store

Avant de parler des propriétés calculées, parlons d'abord des propriétés d'état. Chaque propriété définie dans votre état devient un signal accessible directement depuis l'instance du store :

ts
import { Component, inject } from '@angular/core';
import { UserStore } from './user.store';

@Component({
  selector: 'app-user-list',
  template: `
    @if (userStore.isLoading()) {
      <div>Chargement...</div>
    } @else if (userStore.error()) {
      <div>{{ userStore.error() }}</div>
    } @else {
      <ul>
    <ul>
      @for (user of userStore.users(); track user.id) {
        <li [class.selected]="user.id === userStore.selectedUserId()">
          {{ user.name }}
        </li>
        }
      </ul>
    }
  `
})
export class UserListComponent {
  userStore = inject(UserStore);
}

SIGNALS

Remarquez que l'accès aux propriétés se fait en utilisant des parenthèses (), car chaque propriété est un signal. Cela permet à Angular de suivre automatiquement les dépendances pour un rendu optimal.

Propriétés calculées avec withComputed

withComputed prend une fonction qui reçoit le store en paramètre et peut être utilisé de deux façons : soit en retournant directement un objet de propriétés calculées, soit en créant des variables intermédiaires pour une meilleure réutilisation.

Avec des variables intermédiaires :

ts
import { signalStore, withState, withComputed } from '@ngrx/signals';
import { computed } from '@angular/core';

interface ProductState {
  products: Product[];
  filter: string;
  sortBy: 'price' | 'name';
}

export const ProductStore = signalStore(
  withState<ProductState>({
    products: [],
    filter: '',
    sortBy: 'name'
  }),
  
  // Propriétés calculées avec variables intermédiaires
  withComputed((store) => {
    // Création de variables calculées intermédiaires
    const filteredProducts = computed(() => {
      const products = store.products();
      const filter = store.filter().toLowerCase();
      
      // Filtrer selon le terme de recherche
      return filter
        ? products.filter(p => p.name.toLowerCase().includes(filter))
        : products;
    });
    
    // Variable pour le tri des produits
    const sortedProducts = computed(() => {
      const products = filteredProducts(); // Utilisation d'une autre variable calculée
      
      return [...products].sort((a, b) => {
        if (store.sortBy() === 'price') {
          return a.price - b.price;
        } else {
          return a.name.localeCompare(b.name);
        }
      });
    });
    
    // On retourne l'objet des propriétés exposées
    return {
      // Exposer les variables calculées
      filteredProducts,
      sortedProducts,
      
      // Ajouter d'autres calculs qui utilisent les variables intermédiaires
      totalPrice: computed(() => 
        filteredProducts().reduce((sum, product) => sum + product.price, 0)
      ),
      
      productCount: computed(() => filteredProducts().length),
      
      hasResults: computed(() => sortedProducts().length > 0)
    };
  })
);

Cette approche présente plusieurs avantages :

  1. Réutilisation des calculs : Les variables intermédiaires peuvent être utilisées dans plusieurs autres calculs, évitant ainsi la duplication de code.

  2. Lisibilité améliorée : Le code est plus facile à comprendre car les calculs sont décomposés en étapes logiques.

  3. Optimisation des performances : Grâce à la memoïsation des signaux, les calculs intermédiaires ne sont exécutés qu'une seule fois, même s'ils sont utilisés dans plusieurs propriétés calculées.

L'utilisation depuis les composants reste identique :

ts
import { Component, inject } from '@angular/core';
import { ProductStore } from './product.store';

@Component({
  template: `
    <div>Nombre de produits: {{ productStore.productCount() }}</div>
    <div>Prix total: {{ productStore.totalPrice() }} €</div>
    
    <ul>
      @for (product of productStore.sortedProducts(); track product.id) {
        <li>{{ product.name }} - {{ product.price }} €</li>
      }
    </ul>
  `
})
export class ProductListComponent {
  productStore = inject(ProductStore);
}

ASTUCE

Cette approche est particulièrement utile pour des stores complexes où plusieurs propriétés calculées dépendent des mêmes transformations intermédiaires. Elle permet d'optimiser les performances en évitant de recalculer les mêmes résultats.

Avec retour direct d'objet (approche classique) :

ts
import { signalStore, withState, withComputed } from '@ngrx/signals';
import { computed } from '@angular/core';

interface ProductState {
  products: Product[];
  filter: string;
  sortBy: 'price' | 'name';
}

export const ProductStore = signalStore(
  withState<ProductState>({
    products: [],
    filter: '',
    sortBy: 'name'
  }),
  
  // Propriétés calculées
  withComputed((store) => ({
    // Filtrer et trier les produits
    filteredProducts: computed(() => {
      const products = store.products();
      const filter = store.filter().toLowerCase();
      
      // Filtrer d'abord
      let result = filter
        ? products.filter(p => p.name.toLowerCase().includes(filter))
        : products;
        
      // Puis trier
      return [...result].sort((a, b) => {
        if (store.sortBy() === 'price') {
          return a.price - b.price;
        } else {
          return a.name.localeCompare(b.name);
        }
      });
    }),
    
    // Calculer le prix total
    totalPrice: computed(() => 
      store.products().reduce((sum, product) => sum + product.price, 0)
    ),
    
    // Vérifier s'il y a des résultats
    hasResults: computed(() => store.filteredProducts().length > 0)
  }))
);

L'utilisation est identique aux propriétés d'état standard :

ts
import { Component, inject } from '@angular/core';
import { ProductStore } from './product.store';

@Component({
  template: `
    <div>Prix total: {{ productStore.totalPrice() }} €</div>
    
    <input 
      placeholder="Filtrer..." 
      [ngModel]="productStore.filter()" 
      (ngModelChange)="updateFilter($event)" 
    />
    
    @if (productStore.hasResults()) {
      <ul>
        @for (product of productStore.filteredProducts(); track product.id) {
          <li>{{ product.name }} - {{ product.price }} €</li>
        }
      </ul>
    } @else {
      <p>Aucun produit ne correspond à votre recherche</p>
    }
  `
})
export class ProductListComponent {
  productStore = inject(ProductStore);
  
  updateFilter(filter: string) {
    // Méthode pour mettre à jour le filtre (expliqué dans la section "write")
  }
}

Sélecteurs paramétrés

Les propriétés calculées sont puissantes, mais parfois vous avez besoin de sélecteurs paramétrés, comme trouver un élément par ID. Voici comment les implémenter :

ts
import { signalStore, withState, withMethods } from '@ngrx/signals';
import { computed } from '@angular/core';

export const UserStore = signalStore(
  withState<UserState>({ users: [] }),
  withMethods((store) => ({
    // Sélecteur paramétré pour trouver un utilisateur par ID
    getUserById(id: number) {
      return computed(() => store.users().find(user => user.id === id) || null);
    }
  }))
);

Utilisation :

ts
import { Component, inject } from '@angular/core';
import { UserStore } from './user.store';

@Component({
  template: `
    @if (user(); as userData) {
      <div>
        <h2>{{ userData.name }}</h2>
        <p>{{ userData.email }}</p>
      </div>
    } @else {
      <p>Utilisateur non trouvé</p>
    }
  `
})
export class UserDetailComponent {
  userStore = inject(UserStore);
  user = this.userStore.getUserById(123); // Signal qui suit un utilisateur spécifique
}

PERFORMANCE

Attention aux sélecteurs paramétrés : ils créent un nouveau signal à chaque appel. Pour des scénarios à haute performance, envisagez de mettre en cache les résultats ou d'utiliser des techniques plus avancées.

Combiner avec d'autres sources

Vous pouvez combiner des signaux de votre store avec d'autres sources de données :

ts
import { Component, inject, computed, signal } from '@angular/core';
import { UserStore } from './user.store';

@Component({
  // ...
})
export class UserDashboardComponent {
  userStore = inject(UserStore);
  
  // Un signal local
  showDetails = signal(false);
  
  // Combiner des signaux du store avec des signaux locaux
  displayData = computed(() => {
    const users = this.userStore.filteredUsers();
    return {
      count: users.length,
      names: this.showDetails() ? users.map(u => u.name).join(', ') : 'Cliquez pour afficher',
      isLoading: this.userStore.isLoading()
    };
  });
}

Interopérabilité avec RxJS

Pour les cas où vous avez besoin d'interagir avec des composants basés sur RxJS, NgRx Signal Store offre une interopérabilité avec les Observables :

ts
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { map } from 'rxjs/operators';
import { UserStore } from './user.store';

@Component({
  // ...
})
export class UserComponent {
  userStore = inject(UserStore);
  
  // Convertir un signal en Observable
  users$ = toObservable(this.userStore.users);
  
  // Vous pouvez ensuite utiliser les opérateurs RxJS
  filteredUsers$ = this.users$.pipe(
    map(users => users.filter(/* ... */))
  );
  
  // Et convertir à nouveau en signal si nécessaire
  displayUsers = toSignal(this.filteredUsers$, { initialValue: [] });
}

RxJS INTEROP

Pour plus de détails sur l'interopérabilité entre signaux et RxJS, consultez notre tutoriel dédié sur l'interopérabilité des signaux avec RxJS.

Exemple complet

Voici un exemple complet de lecture d'état dans un composant de catalogue de produits :