Appearance
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 :
Réutilisation des calculs : Les variables intermédiaires peuvent être utilisées dans plusieurs autres calculs, évitant ainsi la duplication de code.
Lisibilité améliorée : Le code est plus facile à comprendre car les calculs sont décomposés en étapes logiques.
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 :