@dsivd/prestations-ng 19.2.0-beta.12 → 19.2.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,60 +1,60 @@
1
- # Introduction aux Signaux Angular
1
+ # Introduction to Angular Signals
2
2
 
3
- **Objectif** : compréhension solide de `signal()`, `computed()`, `effect()`, `input()`/`model()` et `linkedSignal()`, ainsi que des réflexes pour éviter les pièges les plus fréquents (mutation vs réassignation, sur-tracking dans `effect()`).
3
+ **Goal**: a solid understanding of `signal()`, `computed()`, `effect()`, `input()`/`model()` and `linkedSignal()`, as well as the reflexes to avoid the most frequent pitfalls (mutation vs reassignment, over-tracking in `effect()`).
4
4
 
5
5
  ---
6
6
 
7
- ## Plan de session
7
+ ## Session outline
8
8
 
9
- | # | Bloc |
10
- | --- | ----------------------------------------------------------- |
11
- | 1. | Pourquoi les signaux ? |
12
- | 2. | Les 3 primitives : `signal()`, `computed()`, `effect()` |
13
- | 3. | `input()` et `model()` : le nouveau contrat des composants |
14
- | 4. | `linkedSignal()` : l'état local qui se resynchronise |
15
- | 5. | `untracked()` : maîtriser les dépendances d'un `effect()` |
16
- | 6. | RxJS interop : signaux **et** RxJS, pas l'un contre l'autre |
17
- | 7. | Le piège n°1 : mutation vs réassignation |
9
+ | # | Block |
10
+ | --- | ------------------------------------------------------------- |
11
+ | 1. | Why signals? |
12
+ | 2. | The 3 primitives: `signal()`, `computed()`, `effect()` |
13
+ | 3. | `input()` and `model()`: the new contract of components |
14
+ | 4. | `linkedSignal()`: local state that resynchronizes |
15
+ | 5. | `untracked()`: controlling the dependencies of an `effect()` |
16
+ | 6. | RxJS interop: signals **and** RxJS, not one against the other |
17
+ | 7. | Pitfall #1: mutation vs reassignment |
18
18
 
19
19
  ---
20
20
 
21
- ## 1. Pourquoi les signaux ?
21
+ ## 1. Why signals?
22
22
 
23
- **Avant** : Angular détecte les changements en réexécutant (potentiellement) **tout l'arbre de composants** à chaque événement asynchrone intercepté par **Zone.js** (clic, timer, XHR...). Efficace historiquement, mais coûteux et peu prévisible à grande échelle.
23
+ **Before**: Angular detects changes by (potentially) re-running **the whole component tree** on every asynchronous event intercepted by **Zone.js** (click, timer, XHR...). Historically effective, but costly and hard to predict at scale.
24
24
 
25
- **Avec les signaux** : chaque valeur réactive **déclare explicitement** qui en dépend. Angular ne recalcule/ré-affiche que ce qui a réellement changé. C'est un modèle **fine-grained** et **déclaratif**.
25
+ **With signals**: each reactive value **explicitly declares** what depends on it. Angular only recomputes/re-renders what has actually changed. It's a **fine-grained** and **declarative** model.
26
26
 
27
- > 🎤 on ne "déclenche" plus la détection de changement, on **décrit un flux de données**. Le framework se charge de propager.
27
+ > 🎤 we no longer "trigger" change detection, we **describe a data flow**. The framework takes care of propagating it.
28
28
 
29
29
  ---
30
30
 
31
- ## 2. Les 3 primitives de base
31
+ ## 2. The 3 basic primitives
32
32
 
33
- ### `signal()` — une valeur réactive modifiable
33
+ ### `signal()` — a writable reactive value
34
34
 
35
35
  ```ts
36
36
  readonly count = signal(0);
37
37
 
38
38
  increment(): void {
39
39
  this.count.update(v => v + 1);
40
- // ou, pour remplacer complètement la valeur :
40
+ // or, to replace the value completely:
41
41
  this.count.set(5);
42
42
  }
43
43
  ```
44
44
 
45
- ⚠️ **il n'y a pas de `.mutate()`**. C'était dans la proposition initiale, retiré avant la version stable. On ne modifie jamais la valeur en place — on refait toujours passer par `.set()` / `.update()` avec une nouvelle référence (on y revient en détail au bloc 6).
45
+ ⚠️ **there is no `.mutate()`**. It was in the initial proposal, and removed before the stable release. You never modify the value in place — you always go through `.set()` / `.update()` with a new reference (covered in detail in block 7).
46
46
 
47
- ### `computed()` — une valeur dérivée, en lecture seule
47
+ ### `computed()` — a derived, read-only value
48
48
 
49
49
  ```ts
50
50
  readonly fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
51
51
  ```
52
52
 
53
- - Pure et synchrone, jamais de `.set()` à l'intérieur
54
- - Paresseux et mémoïsé : ne se recalcule que si une dépendance change
55
- - Les dépendances sont détectées **automatiquement** — pas besoin de les déclarer
53
+ - Pure and synchronous, never a `.set()` inside
54
+ - Lazy and memoized: only recomputed when a dependency changes
55
+ - Dependencies are detected **automatically** — no need to declare them
56
56
 
57
- ### `effect()` — un effet de bord
57
+ ### `effect()` — a side effect
58
58
 
59
59
  ```ts
60
60
  effect(() => {
@@ -62,17 +62,17 @@ effect(() => {
62
62
  });
63
63
  ```
64
64
 
65
- - S'exécute au moins une fois à l'initialisation
66
- - Se nettoie automatiquement à la destruction du composant
67
- - **Tout signal lu de façon synchrone dans le corps devient une dépendance** — y compris ceux qu'on ne voulait pas "trigger" (on y revient au bloc 5)
65
+ - Runs at least once at initialization
66
+ - Cleans itself up automatically when the component is destroyed
67
+ - **Every signal read synchronously in its body becomes a dependency** — including the ones you didn't want to "trigger" it (covered in block 5)
68
68
 
69
- **Exemple concret — les trois primitives ensemble :**
69
+ **Concrete example — the three primitives together:**
70
70
 
71
71
  ```ts
72
72
  @Component({
73
73
  selector: 'app-counter',
74
74
  template: `
75
- <p>Count : {{ count() }} ({{ isEven() ? 'pair' : 'impair' }})</p>
75
+ <p>Count: {{ count() }} ({{ isEven() ? 'even' : 'odd' }})</p>
76
76
  <button (click)="increment()">+1</button>
77
77
  `,
78
78
  })
@@ -81,7 +81,7 @@ export class CounterComponent {
81
81
  readonly isEven = computed(() => this.count() % 2 === 0);
82
82
 
83
83
  constructor() {
84
- // se déclenche à l'init, puis à chaque changement de `count`
84
+ // runs at init, then on every change of `count`
85
85
  effect(() => console.log(`count = ${this.count()}`));
86
86
  }
87
87
 
@@ -91,68 +91,68 @@ export class CounterComponent {
91
91
  }
92
92
  ```
93
93
 
94
- Remarque : `isEven` ne se recalcule que quand `count` change (mémoïsation), et le template ne se ré-affiche que sur la portion concernée — pas de re-render de tout le composant.
94
+ Note: `isEven` is only recomputed when `count` changes (memoization), and the template only re-renders the affected part — no re-render of the whole component.
95
95
 
96
- ### Mental model à donner à l'équipe
96
+ ### Mental model to give the team
97
97
 
98
- | Je veux... | J'utilise... |
99
- | ---------------------------------------------------- | ------------ |
100
- | Transformer / dériver une donnée | `computed()` |
101
- | Synchroniser avec le DOM, un service externe, logger | `effect()` |
98
+ | I want to... | I use... |
99
+ | -------------------------------------------------- | ------------ |
100
+ | Transform / derive data | `computed()` |
101
+ | Synchronize with the DOM, an external service, log | `effect()` |
102
102
 
103
- 🚩 **Le red flag à apprendre à repérer** : un `effect()` qui fait `.set()` sur un autre signal est presque toujours un `computed()` déguisé.
103
+ 🚩 **The red flag to learn to spot**: an `effect()` that calls `.set()` on another signal is almost always a disguised `computed()`.
104
104
 
105
105
  ```ts
106
- // 🔴 À corriger dès que vu en review
106
+ // 🔴 To fix as soon as seen in review
107
107
  effect(() => {
108
108
  this.minYear.set(this.minDate()?.[0] ?? dayjs().year() - 100);
109
109
  });
110
110
 
111
- // 🟢 Ce que c'est vraiment
111
+ // 🟢 What it really is
112
112
  readonly minYear = computed(() => this.minDate()?.[0] ?? dayjs().year() - 100);
113
113
  ```
114
114
 
115
115
  ---
116
116
 
117
- ## 3. `input()` et `model()` : le nouveau contrat des composants
117
+ ## 3. `input()` and `model()`: the new contract of components
118
118
 
119
- ### `input()` remplace `@Input()` (binding parent → enfant, une direction)
119
+ ### `input()` replaces `@Input()` (parent → child binding, one direction)
120
120
 
121
121
  ```ts
122
- // Avant
122
+ // Before
123
123
  @Input() closeable = true;
124
124
 
125
- // Après
125
+ // After
126
126
  readonly closeable = input(true);
127
127
  ```
128
128
 
129
- ### `model()` remplace la paire `@Input()` + `@Output()` (binding bidirectionnel)
129
+ ### `model()` replaces the `@Input()` + `@Output()` pair (two-way binding)
130
130
 
131
131
  ```ts
132
- // Avant
132
+ // Before
133
133
  @Input() isVisible = false;
134
134
  @Output() isVisibleChange = new EventEmitter<boolean>();
135
135
 
136
- // Après
136
+ // After
137
137
  readonly isVisible = model(false);
138
138
  ```
139
139
 
140
140
  ```html
141
- <!-- deux sens -->
141
+ <!-- both ways -->
142
142
  <my-component [(isVisible)]="open" />
143
143
  ```
144
144
 
145
- | Scénario | Outil |
146
- | ------------------------------------------------------- | ---------------- |
147
- | Le parent écrit, l'enfant lit seulement | `input()` |
148
- | Le parent écrit, l'enfant réécrit, le parent lit | `model()` |
149
- | Le parent écrit, l'enfant réécrit, le parent ne lit pas | `linkedSignal()` |
150
- | Dérivé d'autres inputs | `computed()` |
145
+ | Scenario | Tool |
146
+ | ----------------------------------------------------------------- | ---------------- |
147
+ | The parent writes, the child only reads | `input()` |
148
+ | The parent writes, the child writes back, the parent reads | `model()` |
149
+ | The parent writes, the child writes back, the parent doesn't read | `linkedSignal()` |
150
+ | Derived from other inputs | `computed()` |
151
151
 
152
- **Exemple concret — avant/après sur un composant type modale :**
152
+ **Concrete example — before/after on a modal-like component:**
153
153
 
154
154
  ```ts
155
- // 🔴 Avant : @Input()/@Output() + synchronisation manuelle
155
+ // 🔴 Before: @Input()/@Output() + manual synchronization
156
156
  @Input() isVisible = false;
157
157
  @Output() isVisibleChange = new EventEmitter<boolean>();
158
158
 
@@ -163,7 +163,7 @@ close(): void {
163
163
  ```
164
164
 
165
165
  ```ts
166
- // 🟢 Après : model()
166
+ // 🟢 After: model()
167
167
  readonly isVisible = model(false);
168
168
 
169
169
  close(): void {
@@ -172,26 +172,26 @@ close(): void {
172
172
  ```
173
173
 
174
174
  ```html
175
- <!-- Côté parent : aucun code de synchronisation à écrire -->
175
+ <!-- On the parent side: no synchronization code to write -->
176
176
  <app-modal [(isVisible)]="showModal" />
177
177
  ```
178
178
 
179
179
  ---
180
180
 
181
- ## 4. `linkedSignal()` : l'état local qui se resynchronise
181
+ ## 4. `linkedSignal()`: local state that resynchronizes
182
182
 
183
- Le chaînon manquant entre `computed()` (lecture seule) et `signal()` (aucune synchro automatique) : une valeur **modifiable localement**, mais qui **se réinitialise** quand une source change.
183
+ The missing link between `computed()` (read-only) and `signal()` (no automatic synchronization): a value that is **writable locally**, but that **resets** when a source changes.
184
184
 
185
185
  ```ts
186
186
  readonly options = input<string[]>([]);
187
187
  readonly selectedOption = linkedSignal(() => this.options()[0]);
188
188
 
189
- // L'utilisateur peut toujours écrire dessus...
189
+ // The user can still write to it...
190
190
  this.selectedOption.set('other value');
191
- // ...jusqu'à ce que `options` change, auquel cas ça se réinitialise.
191
+ // ...until `options` changes, in which case it resets.
192
192
  ```
193
193
 
194
- 🚩 **Red flag à apprendre** : un `effect()` qui reset un `signal()` classique quand une autre valeur change est un `linkedSignal()` qui s'ignore.
194
+ 🚩 **Red flag to learn**: an `effect()` that resets a plain `signal()` when another value changes is a `linkedSignal()` in disguise.
195
195
 
196
196
  ```ts
197
197
  // 🔴
@@ -202,7 +202,7 @@ effect(() => this.selectedOption.set(this.options()[0]));
202
202
  readonly selectedOption = linkedSignal(() => this.options()[0]);
203
203
  ```
204
204
 
205
- **Exemple concret — un `select` dont les options viennent du parent :**
205
+ **Concrete example — a `select` whose options come from the parent:**
206
206
 
207
207
  ```ts
208
208
  @Component({
@@ -221,100 +221,100 @@ readonly selectedOption = linkedSignal(() => this.options()[0]);
221
221
  export class BranchSelectComponent {
222
222
  readonly branches = input<string[]>([]);
223
223
 
224
- // se réinitialise sur le premier élément à chaque changement de `branches`,
225
- // mais reste librement modifiable par l'utilisateur entre deux changements
224
+ // resets to the first element on every change of `branches`,
225
+ // but stays freely writable by the user between two changes
226
226
  readonly selectedBranch = linkedSignal(() => this.branches()[0]);
227
227
  }
228
228
  ```
229
229
 
230
- Sans `linkedSignal()`, il faudrait un `effect()` qui fait `.set()` sur un `signal()` classique à chaque changement de `branches` — exactement le red flag vu plus haut.
230
+ Without `linkedSignal()`, you would need an `effect()` calling `.set()` on a plain `signal()` on every change of `branches` — exactly the red flag seen above.
231
231
 
232
232
  ---
233
233
 
234
- ## 5. `untracked()` : maîtriser les dépendances d'un `effect()`
234
+ ## 5. `untracked()`: controlling the dependencies of an `effect()`
235
235
 
236
- Un `effect()` s'abonne à **tout** signal lu de façon synchrone dans son corps — pas seulement à ceux qu'on avait en tête comme déclencheurs. `untracked()` permet de lire une valeur **sans** créer cette dépendance.
236
+ An `effect()` subscribes to **every** signal read synchronously in its body — not only to the ones you had in mind as triggers. `untracked()` reads a value **without** creating this dependency.
237
237
 
238
238
  ```ts
239
239
  effect(() => {
240
- const id = this.userId(); // tracké
241
- const config = untracked(() => this.appConfig()); // lu, mais pas tracké
240
+ const id = this.userId(); // tracked
241
+ const config = untracked(() => this.appConfig()); // read, but not tracked
242
242
  this.analytics.logUserView(id, config);
243
243
  });
244
244
  ```
245
245
 
246
- Cas d'usage le plus fréquent dans nos composants : un `effect()` qui lit **et** réécrit potentiellement le même signal (ex. corriger un `model()` de texte selon un préfixe).
246
+ Most frequent use case in our components: an `effect()` that reads **and** possibly writes back the same signal (e.g. fixing a text `model()` according to a prefix).
247
247
 
248
- ⚠️ **Piège à ne pas commettre** : ici, il ne faut **pas** faire `untracked(() => this.value())` sur le signal principal qu'on veut corriger — si on l'untrack, l'effect ne sera plus jamais redéclenché quand `value` change ailleurs (ex. la frappe de l'utilisateur), donc plus aucune correction ne se produira après le premier passage. `untracked()` sert à exclure des **signaux secondaires** qui ne doivent pas déclencher l'effect (un flag de config par exemple) — pas le signal principal qu'on observe.
248
+ ⚠️ **Pitfall to avoid**: here, you must **not** do `untracked(() => this.value())` on the main signal you want to fix — if you untrack it, the effect will never be triggered again when `value` changes elsewhere (e.g. the user typing), so no fix will happen anymore after the first run. `untracked()` is for excluding **secondary signals** that must not trigger the effect (a configuration flag for example) — not the main signal being observed.
249
249
 
250
- La sécurité contre la boucle infinie ne vient pas d'`untracked()` dans ce cas, mais de la **convergence** : l'effet ne réécrit que si une condition change réellement, donc le second passage (déclenché par sa propre écriture) devient un no-op.
250
+ The protection against the infinite loop doesn't come from `untracked()` in this case, but from **convergence**: the effect only writes if a condition really changes, so the second run (triggered by its own write) becomes a no-op.
251
251
 
252
252
  ```ts
253
- private previousPrefix = DEFAULT_PREFIX; // simple champ de classe, pas un signal : pas besoin de réactivité dessus
253
+ private previousPrefix = DEFAULT_PREFIX; // plain class field, not a signal: no need for reactivity on it
254
254
 
255
255
  effect(() => {
256
- let valueCopy = this.value(); // tracké : l'effect DOIT réagir aux changements de value
257
- const prefix = this.prefix(); // tracké : l'effect DOIT réagir aux changements de prefix
256
+ let valueCopy = this.value(); // tracked: the effect MUST react to the changes of value
257
+ const prefix = this.prefix(); // tracked: the effect MUST react to the changes of prefix
258
258
 
259
- // signal secondaire, hors sujet pour le déclenchement de cet effect : untracked() légitime
259
+ // secondary signal, irrelevant for triggering this effect: untracked() is legitimate
260
260
  if (untracked(() => this.trimOnBlur())) {
261
261
  valueCopy = valueCopy?.trim() ?? null;
262
262
  }
263
263
 
264
- // Le prefix a changé : on corrige value et on notifie le parent
264
+ // The prefix has changed: fix value and notify the parent
265
265
  if (!valueCopy?.includes(prefix)) {
266
266
  const withoutOldPrefix = valueCopy?.includes(this.previousPrefix)
267
267
  ? valueCopy.slice(this.previousPrefix.length)
268
268
  : valueCopy;
269
269
 
270
- this.value.set(withoutOldPrefix); // re-déclenche cet effect (voulu, cf. commentaire ci-dessous)
271
- this.onUserInput(withoutOldPrefix); // vrai effet de bord (ex: notifier le parent)
270
+ this.value.set(withoutOldPrefix); // triggers this effect again (intended, see the comment below)
271
+ this.onUserInput(withoutOldPrefix); // real side effect (e.g. notify the parent)
272
272
  }
273
273
 
274
- // ⚠️ Ce .set() ci-dessus re-déclenche cet effect une 2e fois (attendu) :
275
- // au 2e passage, valueCopy.includes(prefix) devient vrai → plus d'écriture → convergence.
274
+ // ⚠️ The .set() above triggers this effect a 2nd time (expected):
275
+ // on the 2nd run, valueCopy.includes(prefix) becomes true → no more write → convergence.
276
276
  this.previousPrefix = prefix;
277
277
  });
278
278
  ```
279
279
 
280
- Ce qui rend ce `.set()` sur `value` légitime malgré le red flag de la section 2 : l'effect fait aussi `onUserInput()`, un vrai effet de bord (notifier le parent) — ce n'est pas une dérivation pure, donc pas un `computed()` déguisé.
280
+ What makes this `.set()` on `value` legitimate despite the red flag of section 2: the effect also calls `onUserInput()`, a real side effect (notifying the parent) — it's not a pure derivation, so not a disguised `computed()`.
281
281
 
282
- ⚠️ `untracked()` ne remplace pas une garde de convergence. Il contrôle **ce qui déclenche** l'effet (signaux secondaires à exclure), pas le fait qu'une écriture sur le signal principal doive être conditionnelle pour éviter une boucle infinie — ce sont deux problèmes différents, qui se combinent souvent dans le même effect comme ci-dessus.
282
+ ⚠️ `untracked()` doesn't replace a convergence guard. It controls **what triggers** the effect (secondary signals to exclude), not the fact that a write on the main signal must be conditional to avoid an infinite loop — these are two different problems, which are often combined in the same effect as above.
283
283
 
284
284
  ---
285
285
 
286
- ## 6. RxJS interop : signaux **et** RxJS, pas l'un contre l'autre
286
+ ## 6. RxJS interop: signals **and** RxJS, not one against the other
287
287
 
288
- > 🎤 **on ne migre pas tout le RxJS existant vers des signaux**. Les deux modèles répondent à des besoins différents, et le mélanger au mauvais endroit est aussi un piège.
288
+ > 🎤 **we don't migrate all the existing RxJS to signals**. Both models answer different needs, and mixing them in the wrong place is a pitfall too.
289
289
 
290
- | | Signal | Observable (RxJS) |
291
- | --------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
292
- | Modèle | **État** — une valeur à un instant T | **Flux** — une séquence de valeurs dans le temps |
293
- | Mode | Pull (on lit `signal()` quand on veut) | Push (on s'abonne, on reçoit) |
294
- | Fort pour | État de composant, dérivations synchrones, template | Async (HTTP, WebSocket), opérateurs (`debounceTime`, `switchMap`, `retry`, `combineLatest`...), annulation de requêtes |
290
+ | | Signal | Observable (RxJS) |
291
+ | ---------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
292
+ | Model | **State** — a value at a time T | **Stream** — a sequence of values over time |
293
+ | Mode | Pull (you read `signal()` when you want) | Push (you subscribe, you receive) |
294
+ | Strong for | Component state, synchronous derivations, template | Async (HTTP, WebSocket), operators (`debounceTime`, `switchMap`, `retry`, `combineLatest`...), request cancellation |
295
295
 
296
- **Règle simple à donner à l'équipe** : si la donnée représente "un état actuel", signal. Si elle représente "une séquence d'événements dans le temps" avec de la composition/annulation à gérer, RxJS reste le bon outil — un appel HTTP en est l'exemple typique.
296
+ **Simple rule to give the team**: if the data represents "a current state", signal. If it represents "a sequence of events over time" with composition/cancellation to handle, RxJS remains the right tool — an HTTP call is the typical example.
297
297
 
298
- ### `toSignal()` — exposer un Observable comme un Signal
298
+ ### `toSignal()` — exposing an Observable as a Signal
299
299
 
300
- Le cas le plus courant : un service qui expose un appel HTTP en `Observable`, consommé comme un `Signal` côté composant pour éviter le `| async` dans le template.
300
+ The most common case: a service exposing an HTTP call as an `Observable`, consumed as a `Signal` in the component to avoid the `| async` in the template.
301
301
 
302
302
  ```ts
303
303
  import { toSignal } from '@angular/core/rxjs-interop';
304
304
 
305
305
  private readonly userService = inject(UserService);
306
306
 
307
- // userService.getUser() retourne un Observable<User> (appel HTTP RxJS, inchangé)
307
+ // userService.getUser() returns an Observable<User> (RxJS HTTP call, unchanged)
308
308
  readonly user = toSignal(this.userService.getUser(), { initialValue: null });
309
309
  ```
310
310
 
311
- - Se comporte comme le `| async` du template, mais utilisable partout (pas seulement en template)
312
- - Si l'Observable complète, le signal garde la dernière valeur émise
313
- - Si l'Observable erreur, l'erreur est relancée à la lecture du signal
311
+ - Behaves like the `| async` of the template, but usable everywhere (not only in the template)
312
+ - If the Observable completes, the signal keeps the last emitted value
313
+ - If the Observable errors, the error is thrown again when the signal is read
314
314
 
315
- ### `toObservable()` — exposer un Signal comme un Observable
315
+ ### `toObservable()` — exposing a Signal as an Observable
316
316
 
317
- Le sens inverse : utile quand on veut piper un signal (ex. une valeur de recherche tapée par l'utilisateur) dans des opérateurs RxJS avant de déclencher un appel HTTP.
317
+ The other way around: useful when you want to pipe a signal (e.g. a search value typed by the user) into RxJS operators before triggering an HTTP call.
318
318
 
319
319
  ```ts
320
320
  import { toObservable, toSignal } from '@angular/core/rxjs-interop';
@@ -322,7 +322,7 @@ import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
322
322
 
323
323
  readonly query = signal('');
324
324
 
325
- // on garde RxJS pour ce que RxJS fait bien : debounce + annulation de requêtes en vol
325
+ // we keep RxJS for what RxJS does well: debounce + cancellation of in-flight requests
326
326
  private readonly results$ = toObservable(this.query).pipe(
327
327
  debounceTime(300),
328
328
  distinctUntilChanged(),
@@ -332,13 +332,13 @@ private readonly results$ = toObservable(this.query).pipe(
332
332
  readonly results = toSignal(this.results$, { initialValue: [] });
333
333
  ```
334
334
 
335
- C'est l'exemple le plus parlant à montrer à l'équipe : **le signal pilote l'input utilisateur, RxJS gère le debounce/switchMap/annulation, et on ressort un signal côté template.** Aucun des deux modèles n'est sacrifié.
335
+ It's the most telling example to show the team: **the signal drives the user input, RxJS handles the debounce/switchMap/cancellation, and a signal comes out for the template.** Neither model is sacrificed.
336
336
 
337
- ⚠️ `toObservable()` doit être déclaré comme **champ de classe** (et non appelé à l'intérieur d'une méthode) car il a besoin d'un contexte d'injection au moment de sa création.
337
+ ⚠️ `toObservable()` must be declared as a **class field** (and not called inside a method) because it needs an injection context when it is created.
338
338
 
339
- ### `rxResource()` — appels asynchrones pilotés par un signal, avec état loading/error inclus
339
+ ### `rxResource()` — asynchronous calls driven by a signal, with loading/error state included
340
340
 
341
- Alternative à `toSignal()` quand on veut aussi le suivi d'état (chargement, erreur, rechargement) sans le coder à la main. `rxResource()` accepte un `Observable` comme source de données, et se redéclenche automatiquement quand les signaux dont dépend la `request` changent.
341
+ An alternative to `toSignal()` when you also want state tracking (loading, error, reload) without coding it by hand. `rxResource()` accepts an `Observable` as data source, and is triggered again automatically when the signals the `request` depends on change.
342
342
 
343
343
  ```ts
344
344
  import { rxResource } from '@angular/core/rxjs-interop';
@@ -350,13 +350,13 @@ readonly userResource = rxResource({
350
350
  loader: ({ request: id }) => this.userService.getUser(id), // Observable<User>
351
351
  });
352
352
 
353
- // dans le template : userResource.value(), userResource.isLoading(), userResource.error()
353
+ // in the template: userResource.value(), userResource.isLoading(), userResource.error()
354
354
  ```
355
355
 
356
- **Exemple concret — avant/après sur un appel HTTP géré à la main :**
356
+ **Concrete example — before/after on an HTTP call handled by hand:**
357
357
 
358
358
  ```ts
359
- // 🔴 Avant : subscribe() manuel + variables loading/error gérées à la main
359
+ // 🔴 Before: manual subscribe() + loading/error variables handled by hand
360
360
  readonly user = signal<User | null>(null);
361
361
  readonly loading = signal(false);
362
362
  readonly error = signal<unknown>(null);
@@ -371,7 +371,7 @@ ngOnInit(): void {
371
371
  ```
372
372
 
373
373
  ```ts
374
- // 🟢 Après : rxResource(), loading/error inclus, re-fetch automatique si userId() change
374
+ // 🟢 After: rxResource(), loading/error included, automatic re-fetch if userId() changes
375
375
  readonly userResource = rxResource({
376
376
  request: () => this.userId(),
377
377
  loader: ({ request: id }) => this.userService.getUser(id),
@@ -379,30 +379,30 @@ readonly userResource = rxResource({
379
379
  // userResource.value(), userResource.isLoading(), userResource.error()
380
380
  ```
381
381
 
382
- ### Doit-on abandonner le pipe `async` ?
382
+ ### Should we drop the `async` pipe?
383
383
 
384
- Non, il n'est pas déprécié. Mais la tendance recommandée pour du code neuf est de préférer `toSignal()` dans la plupart des cas :
384
+ No, it isn't deprecated. But the recommended trend for new code is to prefer `toSignal()` in most cases:
385
385
 
386
- - `toSignal()` se comporte comme le pipe `async`, mais la valeur obtenue est utilisable **partout** — pas seulement dans le template (dans un `computed()`, un `effect()`, en `input` d'un enfant...)
387
- - Une **seule souscription** même si la valeur est lue plusieurs fois dans le template (le pipe `async` utilisé deux fois sur le même flux crée deux souscriptions distinctes, sauf `shareReplay`)
388
- - Lectures cohérentes avec le reste du graphe de signaux (important avec `OnPush`/zoneless)
386
+ - `toSignal()` behaves like the `async` pipe, but the value obtained is usable **everywhere** — not only in the template (in a `computed()`, an `effect()`, as `input` of a child...)
387
+ - A **single subscription** even if the value is read several times in the template (the `async` pipe used twice on the same stream creates two distinct subscriptions, unless `shareReplay`)
388
+ - Reads consistent with the rest of the signal graph (important with `OnPush`/zoneless)
389
389
 
390
- Le pipe `async` garde toute sa légitimité quand l'`Observable` n'est consommé **que dans le template**, à un seul endroit, sans que la valeur ait besoin de sortir vers le reste de la classe — convertir en signal dans ce cas n'apporte rien.
390
+ The `async` pipe stays fully legitimate when the `Observable` is consumed **only in the template**, in a single place, without the value needing to go to the rest of the class — converting it to a signal brings nothing in that case.
391
391
 
392
- > 🎤 si la valeur ne sert qu'à l'affichage à un endroit précis, `| async` reste légitime. Si elle sert ailleurs dans le composant (calcul dérivé, passage à un enfant, logique dans un effet), `toSignal()`.
392
+ > 🎤 if the value is only used for display in one specific place, `| async` stays legitimate. If it is used elsewhere in the component (derived computation, passed to a child, logic in an effect), `toSignal()`.
393
393
 
394
- ### Ce qu'on ne migre **pas**
394
+ ### What we **don't** migrate
395
395
 
396
- > 🎤 afin d'éviter la sur-correction après cette session :
396
+ > 🎤 to avoid over-correcting after this session:
397
397
 
398
- - Les appels HTTP restent des `Observable` au niveau service (`HttpClient` retourne du RxJS, on ne change rien ici)
399
- - Les flux avec opérateurs de composition/temporisation (`debounceTime`, `combineLatest`, `retry`, `race`...) restent en RxJS
400
- - On ne convertit un `Observable` en `Signal` (via `toSignal()`) **qu'au bord** — c'est-à-dire au tout dernier moment, là où la donnée quitte le monde RxJS pour devenir un état à afficher, jamais au milieu d'une chaîne d'opérateurs
398
+ - HTTP calls stay `Observable`s at the service level (`HttpClient` returns RxJS, nothing changes here)
399
+ - Streams with composition/timing operators (`debounceTime`, `combineLatest`, `retry`, `race`...) stay in RxJS
400
+ - An `Observable` is converted to a `Signal` (with `toSignal()`) **only at the edge** — that is, at the very last moment, where the data leaves the RxJS world to become a state to display, never in the middle of a chain of operators
401
401
 
402
402
  ```ts
403
- // 🔴 Conversion prématurée en plein milieu du pipeline : un Signal
404
- // n'est pas "pipeable", il faut repasser par toObservable() pour
405
- // continuer la composition — aller-retour inutile
403
+ // 🔴 Premature conversion in the middle of the pipeline: a Signal
404
+ // isn't "pipeable", you have to go back through toObservable() to
405
+ // continue the composition — a useless round trip
406
406
  private readonly debounced = toSignal(
407
407
  toObservable(this.query).pipe(debounceTime(300)),
408
408
  );
@@ -412,7 +412,7 @@ Le pipe `async` garde toute sa légitimité quand l'`Observable` n'est consommé
412
412
  );
413
413
  readonly results = toSignal(this.results$, { initialValue: [] });
414
414
 
415
- // 🟢 Un seul point de conversion, à la toute fin de la chaîne
415
+ // 🟢 A single conversion point, at the very end of the chain
416
416
  private readonly results$ = toObservable(this.query).pipe(
417
417
  debounceTime(300),
418
418
  distinctUntilChanged(),
@@ -421,48 +421,48 @@ Le pipe `async` garde toute sa légitimité quand l'`Observable` n'est consommé
421
421
  readonly results = toSignal(this.results$, { initialValue: [] });
422
422
  ```
423
423
 
424
- Ça vaut aussi entre les couches : un service peut composer entièrement en RxJS (`debounceTime`, `retry`, `combineLatest`...) et rester testable indépendamment de tout composant — c'est le composant consommateur, "au bord", qui convertit en signal au moment de l'affichage.
424
+ It also applies between layers: a service can compose entirely in RxJS (`debounceTime`, `retry`, `combineLatest`...) and stay testable independently of any component — it's the consuming component, "at the edge", that converts to a signal when displaying.
425
425
 
426
426
  ---
427
427
 
428
- ## 7. Le piège n°1 : mutation vs réassignation
428
+ ## 7. Pitfall #1: mutation vs reassignment
429
429
 
430
- C'est **le** réflexe à perdre lorsqu'on vient de l'ère Zone.js : avant, muter un tableau ou un objet imbriqué "finissait par marcher" parce que la détection de changement repassait régulièrement sur tout. **Avec les signaux, ce n'est plus vrai** : seul `.set()` / `.update()` notifie les abonnés. Une mutation en place est invisible pour le signal, le template, et tout `computed()`/`effect()` qui en dépend.
430
+ It's **the** reflex to unlearn when coming from the Zone.js era: before, mutating an array or a nested object "ended up working" because change detection regularly went over everything again. **With signals, this is no longer true**: only `.set()` / `.update()` notify the consumers. An in-place mutation is invisible to the signal, the template, and any `computed()`/`effect()` depending on it.
431
431
 
432
432
  ```ts
433
- // 🔴 No-op silencieux du point de vue du signal
433
+ // 🔴 Silent no-op from the signal's point of view
434
434
  this.address().locality.name = 'newName';
435
435
 
436
- // 🟢 Nouvelle référence de premier niveau
436
+ // 🟢 New top-level reference
437
437
  this.address.update((current) => ({
438
438
  ...current,
439
439
  locality: { ...current.locality, name: 'newName' },
440
440
  }));
441
441
  ```
442
442
 
443
- Même logique pour les méthodes mutantes des tableaux :
443
+ Same logic for the mutating methods of arrays:
444
444
 
445
445
  ```ts
446
- // 🔴 push/splice/pop/shift/unshift/sort/reverse mutent en place
446
+ // 🔴 push/splice/pop/shift/unshift/sort/reverse mutate in place
447
447
  this.model().push({ name: null, address: null });
448
448
 
449
- // 🟢 Toujours une nouvelle référence
449
+ // 🟢 Always a new reference
450
450
  this.model.update((current) => [...current, { name: null, address: null }]);
451
451
  ```
452
452
 
453
- ...et pour `Map` / `Set` (piège sournois : `Map.prototype.set` porte le même nom que le `.set()` du signal) :
453
+ ...and for `Map` / `Set` (a sneaky pitfall: `Map.prototype.set` has the same name as the `.set()` of the signal):
454
454
 
455
455
  ```ts
456
- // 🔴 Ceci est Map.prototype.set, pas celui du signal — mute la même instance
456
+ // 🔴 This is Map.prototype.set, not the signal's — it mutates the same instance
457
457
  this.cache().set(key, value);
458
458
 
459
459
  // 🟢
460
460
  this.cache.update((current) => new Map(current).set(key, value));
461
461
  ```
462
462
 
463
- > 💡 si une méthode modifie "en place" et ne renvoie pas un objet/tableau/Map/Set flambant neuf, elle mute — on la wrap dans une copie. `map`, `filter`, `slice`, `concat`, le spread : toujours sûrs.
463
+ > 💡 if a method modifies "in place" and doesn't return a brand new object/array/Map/Set, it mutates — wrap it in a copy. `map`, `filter`, `slice`, `concat`, the spread: always safe.
464
464
 
465
- **Exemple concret — le plus percutant à montrer à l'équipe :**
465
+ **Concrete example — the most striking one to show the team:**
466
466
 
467
467
  ```ts
468
468
  @Component({
@@ -471,21 +471,21 @@ this.cache.update((current) => new Map(current).set(key, value));
471
471
  @for (contact of contacts(); track contact.id) {
472
472
  <p>{{ contact.name }}</p>
473
473
  }
474
- <button (click)="addBroken()">Ajouter (cassé)</button>
475
- <button (click)="addFixed()">Ajouter (correct)</button>
474
+ <button (click)="addBroken()">Add (broken)</button>
475
+ <button (click)="addFixed()">Add (correct)</button>
476
476
  `,
477
477
  })
478
478
  export class ContactListComponent {
479
479
  readonly contacts = signal<Contact[]>([{ id: 1, name: 'Alice' }]);
480
480
 
481
481
  addBroken(): void {
482
- // 🔴 push() mute le tableau en place : le signal ne détecte rien,
483
- // RIEN ne s'affiche à l'écran malgré la donnée bien présente en mémoire
482
+ // 🔴 push() mutates the array in place: the signal detects nothing,
483
+ // NOTHING is displayed on screen although the data is in memory
484
484
  this.contacts().push({ id: 2, name: 'Bob' });
485
485
  }
486
486
 
487
487
  addFixed(): void {
488
- // 🟢 nouvelle référence : le @for se met à jour normalement
488
+ // 🟢 new reference: the @for updates normally
489
489
  this.contacts.update((current) => [...current, { id: 2, name: 'Bob' }]);
490
490
  }
491
491
  }
@@ -493,11 +493,11 @@ export class ContactListComponent {
493
493
 
494
494
  ---
495
495
 
496
- ## Checklist de fin de session
496
+ ## End-of-session checklist
497
497
 
498
- - Je sais choisir entre `input()`, `model()`, `computed()`, `linkedSignal()` et `signal()`
499
- - Je sais repérer un `effect()` qui devrait être un `computed()`
500
- - Je sais repérer un `effect()` qui devrait être un `linkedSignal()`
501
- - Je ne mute plus jamais une valeur de signal en place — toujours `.set()`/`.update()` avec une nouvelle référence
502
- - Je sais quand utiliser `untracked()` dans un `effect()`
503
- - Je ne cherche pas à remplacer tout mon RxJS par des signaux — je garde RxJS pour l'async composé (HTTP, debounce, switchMap...) et je ne convertis en signal qu'au bord, avec `toSignal()`/`toObservable()`/`rxResource()`
498
+ - I know how to choose between `input()`, `model()`, `computed()`, `linkedSignal()` and `signal()`
499
+ - I know how to spot an `effect()` that should be a `computed()`
500
+ - I know how to spot an `effect()` that should be a `linkedSignal()`
501
+ - I never mutate a signal value in place anymore — always `.set()`/`.update()` with a new reference
502
+ - I know when to use `untracked()` in an `effect()`
503
+ - I don't try to replace all my RxJS with signals — I keep RxJS for composed async (HTTP, debounce, switchMap...) and I only convert to a signal at the edge, with `toSignal()`/`toObservable()`/`rxResource()`