Cours Angular · Module 10: Gestion d'état

SignalStore (NGRX 17+)

1 min de lecture

NgRx SignalStore est une bibliothèque moderne de gestion d'état basée sur les Signals.

Installation et notions de base

1npm install @ngrx/signals
1import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
2import { computed, inject } from '@angular/core';
3
4interface SamuraiState {
5  samurai: Samurai[];
6  loading: boolean;
7  filter: string;
8}
9
10const initialState: SamuraiState = {
11  samurai: [],
12  loading: false,
13  filter: ''
14};
15
16export const SamuraiStore = signalStore(
17  { providedIn: 'root' },
18
19  // État
20  withState(initialState),
21
22  // Signals calculés
23  withComputed((store) => ({
24    filteredSamurai: computed(() => {
25      const filter = store.filter().toLowerCase();
26      return store.samurai().filter(s =>
27        s.name.toLowerCase().includes(filter)
28      );
29    }),
30    count: computed(() => store.samurai().length)
31  })),
32
33  // Méthodes (actions)
34  withMethods((store, http = inject(HttpClient)) => ({
35    loadSamurai(): void {
36      patchState(store, { loading: true });
37
38      http.get<Samurai[]>('/api/samurai').subscribe({
39        next: (samurai) => patchState(store, { samurai, loading: false }),
40        error: () => patchState(store, { loading: false })
41      });
42    },
43
44    setFilter(filter: string): void {
45      patchState(store, { filter });
46    },
47
48    addSamurai(samurai: Samurai): void {
49      patchState(store, (state) => ({
50        samurai: [...state.samurai, samurai]
51      }));
52    }
53  }))
54);
55
56// Utilisation dans un composant
57@Component({
58  template: `
59    <input (input)="store.setFilter($any($event.target).value)">
60    @for (s of store.filteredSamurai(); track s.id) {
61      <div>{{ s.name }}</div>
62    }
63  `
64})
65export class SamuraiListComponent {
66  store = inject(SamuraiStore);
67
68  constructor() {
69    this.store.loadSamurai();
70  }
71}
Code de cette leçon : src/app/app.component.ts
1import { Component, signal } from '@angular/core';
2import { CommonModule } from '@angular/common';
3
4@Component({
5  selector: 'app-root',
6  standalone: true,
7  imports: [CommonModule],
8  template: `
9    <div class="ngrx-guide">
10      <h1>NgRx SignalStore - Guide</h1>
11
12      <section>
13        <h2>Qu’est-ce que NgRx SignalStore ?</h2>
14        <p>NgRx SignalStore est une bibliothèque moderne de gestion d’état qui s’appuie sur les Signals d’Angular. Elle est plus simple que le NgRx Store classique.</p>
15
16        <div class="comparison">
17          <div class="box classic">
18            <h3>NgRx classique</h3>
19            <ul>
20              <li>Actions</li>
21              <li>Reducers</li>
22              <li>Effects</li>
23              <li>Sélecteurs</li>
24              <li>Beaucoup de boilerplate</li>
25            </ul>
26          </div>
27
28          <div class="box signal">
29            <h3>NgRx SignalStore</h3>
30            <ul>
31              <li>withState()</li>
32              <li>withComputed()</li>
33              <li>withMethods()</li>
34              <li>Moins de code</li>
35              <li>Des Signals !</li>
36            </ul>
37          </div>
38        </div>
39      </section>
40
41      <section>
42        <h2>Créer un SignalStore</h2>
43        <pre><code>{{ storeCode }}</code></pre>
44      </section>
45
46      <section>
47        <h2>Utilisation dans un composant</h2>
48        <pre><code>{{ componentCode }}</code></pre>
49      </section>
50
51      <section>
52        <h2>Effets asynchrones</h2>
53        <pre><code>{{ effectsCode }}</code></pre>
54      </section>
55
56      <section>
57        <h2>Configuration</h2>
58        <pre><code>{{ configCode }}</code></pre>
59      </section>
60
61      <div class="tips">
62        <h2>Conseils</h2>
63        <ul>
64          <li><strong>Quand utiliser SignalStore ?</strong> Quand l’état est partagé entre de nombreux composants.</li>
65          <li><strong>Quand les Signals suffisent-ils ?</strong> Pour l’état local d’un composant.</li>
66          <li><strong>Immutabilité :</strong> crée toujours de nouveaux objets au lieu de modifier ceux qui existent.</li>
67          <li><strong>Sélecteurs :</strong> utilise withComputed() pour les données dérivées.</li>
68        </ul>
69      </div>
70    </div>
71  `,
72  styles: [`
73    .ngrx-guide {
74      font-family: 'Segoe UI', sans-serif;
75      padding: 20px;
76      background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
77      min-height: 100vh;
78      color: #eee;
79    }
80
81    h1 {
82      text-align: center;
83      color: #64ffda;
84    }
85
86    h2 {
87      color: #64ffda;
88      border-bottom: 2px solid #64ffda;
89      padding-bottom: 10px;
90    }
91
92    section {
93      background: rgba(0,0,0,0.3);
94      border-radius: 10px;
95      padding: 20px;
96      margin: 20px 0;
97    }
98
99    pre {
100      background: #0d1421;
101      padding: 15px;
102      border-radius: 8px;
103      overflow-x: auto;
104      border-left: 4px solid #64ffda;
105    }
106
107    code {
108      color: #e6e6e6;
109      font-family: 'Fira Code', monospace;
110      font-size: 0.85em;
111      line-height: 1.5;
112    }
113
114    .comparison {
115      display: grid;
116      grid-template-columns: 1fr 1fr;
117      gap: 20px;
118      margin: 20px 0;
119    }
120
121    @media (max-width: 600px) {
122      .comparison {
123        grid-template-columns: 1fr;
124      }
125    }
126
127    .box {
128      padding: 15px;
129      border-radius: 10px;
130    }
131
132    .box.classic {
133      background: rgba(255, 107, 107, 0.2);
134      border: 2px solid #ff6b6b;
135    }
136
137    .box.signal {
138      background: rgba(100, 255, 218, 0.2);
139      border: 2px solid #64ffda;
140    }
141
142    .box h3 {
143      margin-top: 0;
144    }
145
146    .box ul {
147      margin: 0;
148      padding-left: 20px;
149    }
150
151    .box li {
152      margin: 5px 0;
153    }
154
155    .tips {
156      background: rgba(100, 255, 218, 0.1);
157      border: 2px solid #64ffda;
158      border-radius: 10px;
159      padding: 20px;
160    }
161
162    .tips ul {
163      list-style: none;
164      padding: 0;
165    }
166
167    .tips li {
168      padding: 10px 0;
169      border-bottom: 1px solid rgba(100, 255, 218, 0.2);
170    }
171
172    .tips li:last-child {
173      border-bottom: none;
174    }
175  `]
176})
177export class AppComponent {
178  storeCode = `// samurai.store.ts
179import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
180import { computed } from '@angular/core';
181
182interface SamuraiState {
183  samuraiList: Samurai[];
184  selectedId: number | null;
185  loading: boolean;
186}
187
188const initialState: SamuraiState = {
189  samuraiList: [],
190  selectedId: null,
191  loading: false
192};
193
194export const SamuraiStore = signalStore(
195  // État initial
196  withState(initialState),
197
198  // Sélecteurs (computed)
199  withComputed((store) => ({
200    selectedSamurai: computed(() => {
201      const id = store.selectedId();
202      return store.samuraiList().find(s => s.id === id) ?? null;
203    }),
204    totalCount: computed(() => store.samuraiList().length),
205    totalGold: computed(() =>
206      store.samuraiList().reduce((sum, s) => sum + s.gold, 0)
207    )
208  })),
209
210  // Méthodes (actions)
211  withMethods((store) => ({
212    selectSamurai(id: number): void {
213      patchState(store, { selectedId: id });
214    },
215
216    addSamurai(samurai: Samurai): void {
217      patchState(store, {
218        samuraiList: [...store.samuraiList(), samurai]
219      });
220    },
221
222    removeSamurai(id: number): void {
223      patchState(store, {
224        samuraiList: store.samuraiList().filter(s => s.id !== id),
225        selectedId: store.selectedId() === id ? null : store.selectedId()
226      });
227    }
228  }))
229);`;
230
231  componentCode = `// samurai-list.component.ts
232import { Component, inject } from '@angular/core';
233import { SamuraiStore } from './samurai.store';
234
235@Component({
236  selector: 'app-samurai-list',
237  standalone: true,
238  providers: [SamuraiStore], // Ou dans app.config.ts pour un état global
239  template: \`
240    <div>
241      <p>Nombre de samouraïs : {{ store.totalCount() }}</p>
242      <p>Or total : {{ store.totalGold() }}</p>
243
244      @for (samurai of store.samuraiList(); track samurai.id) {
245        <div (click)="store.selectSamurai(samurai.id)">
246          {{ samurai.name }} - {{ samurai.gold }} pièces d’or
247        </div>
248      }
249
250      @if (store.selectedSamurai(); as selected) {
251        <div class="detail">
252          Sélectionné : {{ selected.name }}
253        </div>
254      }
255    </div>
256  \`
257})
258export class SamuraiListComponent {
259  store = inject(SamuraiStore);
260}`;
261
262  effectsCode = `// samurai.store.ts - avec des effets HTTP
263import { signalStore, withState, withMethods } from '@ngrx/signals';
264import { rxMethod } from '@ngrx/signals/rxjs-interop';
265import { inject } from '@angular/core';
266import { HttpClient } from '@angular/common/http';
267import { pipe, switchMap, tap } from 'rxjs';
268
269export const SamuraiStore = signalStore(
270  withState(initialState),
271
272  withMethods((store, http = inject(HttpClient)) => ({
273    // Méthode avec RxJS
274    loadSamurai: rxMethod<void>(
275      pipe(
276        tap(() => patchState(store, { loading: true })),
277        switchMap(() => http.get<Samurai[]>('/api/samurai')),
278        tap(samuraiList => patchState(store, {
279          samuraiList,
280          loading: false
281        }))
282      )
283    ),
284
285    // Méthode async/await
286    async loadSamuraiAsync(): Promise<void> {
287      patchState(store, { loading: true });
288
289      const response = await fetch('/api/samurai');
290      const samuraiList = await response.json();
291
292      patchState(store, { samuraiList, loading: false });
293    }
294  }))
295);`;
296
297  configCode = `// app.config.ts
298import { ApplicationConfig } from '@angular/core';
299import { SamuraiStore } from './stores/samurai.store';
300
301export const appConfig: ApplicationConfig = {
302  providers: [
303    // Store global - disponible dans toute l’application
304    SamuraiStore,
305
306    // Ou avec des feature flags
307    provideStore(),
308    provideState({ name: 'samurai', reducer: samuraiReducer }),
309    provideEffects(SamuraiEffects)
310  ]
311};`;
312}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. À quoi sert la fonction patchState() dans NgRx SignalStore ?

  2. 2. Que définit la fonction withState() dans signalStore() ?

Tâches pratiques dans le jeu

  • Tri horizontal

    Ordonne l'appel signalStore avec ses principaux blocs dans leur ordre d'écriture habituel (une seule ligne, de gauche à droite) :

  • Éditeur de code

    Complète le SignalStore pour gérer les samouraïs.

  • Tri horizontal

    Ordonne la commande pour installer la bibliothèque NgRx SignalStore (une seule ligne, de gauche à droite) :

  • Clique dans l'ordre

    Clique sur les éléments dans l'ordre pour construire un champ qui injecte un SignalStore dans un composant (Angular moderne) :

Articles utiles