Control flow, OnPush and zoneless mode
Key takeaway — The new control flow (
@if,@for,@switch) pairs natively with signals. WithOnPushand, eventually, zoneless mode, Angular only refreshes the views whose signal actually changed.
Signals truly come into their own at render time. This chapter ties reactive state to Angular's modern template and explains how to achieve minimal, performant change detection.
The new control flow
Since Angular 17, the *ngIf and *ngFor directives give way to a syntax built into the template, more readable and faster. It reads signals directly.
@Component({
selector: 'app-root',
standalone: true,
template: `
@if (taches().length === 0) {
<p>No tasks. 🎉</p>
} @else {
<ul>
@for (tache of taches(); track tache.id) {
<li>{{ tache.texte }}</li>
} @empty {
<li>Empty list</li>
}
</ul>
}
`,
})
Key points:
@forrequirestrack. The tracking expression (oftentrack item.id) lets Angular reuse DOM nodes instead of recreating everything. It is mandatory and crucial for performance.@emptyhandles the empty-list case, without an extra@if.@switchreplaces[ngSwitch]:
@switch (statut()) {
@case ('chargement') { <app-spinner /> }
@case ('erreur') { <p>An error occurred.</p> }
@default { <app-liste [donnees]="donnees()" /> }
}
Full example: a reactive task list
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';
interface Tache {
id: number;
texte: string;
faite: boolean;
}
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>Tasks</h1>
<input
#champ
(keyup.enter)="ajouter(champ.value); champ.value = ''"
placeholder="New task then Enter…"
/>
@if (taches().length === 0) {
<p>No tasks for now. 🎉</p>
} @else {
<ul>
@for (tache of taches(); track tache.id) {
<li>
<label>
<input
type="checkbox"
[checked]="tache.faite"
(change)="basculer(tache.id)"
/>
{{ tache.texte }}
</label>
</li>
}
</ul>
<p>{{ restantes() }} task(s) remaining</p>
}
`,
})
export class AppComponent {
private prochainId = 1;
readonly taches = signal<Tache[]>([]);
readonly restantes = computed(() => this.taches().filter((t) => !t.faite).length);
ajouter(texte: string): void {
const valeur = texte.trim();
if (!valeur) return;
this.taches.update((liste) => [
...liste,
{ id: this.prochainId++, texte: valeur, faite: false },
]);
}
basculer(id: number): void {
this.taches.update((liste) =>
liste.map((t) => (t.id === id ? { ...t, faite: !t.faite } : t)),
);
}
}
Try it live
control-flow-taches
Why pair signals and OnPush?
The ChangeDetectionStrategy.OnPush strategy tells Angular not to re-check a component on every global cycle, but only when needed. Now, signals notify precisely the affected view on every change. The combination is ideal:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// …
})
With signals in the template, OnPush introduces no risk of a stale view: any change of a signal read by the view triggers its refresh. That is why all of this course's demos use OnPush. Make it a habit from now on.
Zoneless mode
Historically, Angular detects changes thanks to Zone.js, which "patches" the browser APIs (timers, events, fetch) to trigger detection. It is convenient but global and expensive.
Since signals signal their own changes, Angular can do without Zone.js: that is zoneless mode (stabilized in Angular 20, available in preview before). You enable it at bootstrap:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});
Benefits: lighter bundle (you remove zone.js), more predictable and often faster rendering. Condition: your reactivity must flow through signals (or explicit notifications) rather than "magic" mutations detected by Zone.
In zoneless, a state mutated outside a signal (for example a class property modified in a
setTimeout) no longer refreshes the view automatically. Migrating to signals then becomes the default best practice.
Performance: the right reflexes
- Relevant
trackin every@for(a stable identifier, never the index if the list changes order). computedrather than recomputing in the template (a heavy inline calculation would be re-evaluated on every render).OnPusheverywhere: with signals, it is free in terms of safety.- Split large components: the smaller the views, the more targeted the updates.
Key points
@if/@for(withtrack) /@switch/@empty: modern control flow that reads signals.OnPush+ signals = targeted refreshes, without stale views.- Zoneless mode removes Zone.js and relies on signals.
track,computedand splitting are the performance levers.
Last chapter: assembling all of this into a clean state architecture — signal-based service, linkedSignal, and production best practices.