Angular Signals et Computed en 2026 : Réactivité Fine et Questions d'Entretien
Maîtriser les Signals Angular, computed, effect et linkedSignal dans Angular 20+. Patterns de réactivité fine et préparation aux entretiens techniques.

Les Signals Angular représentent le changement le plus significatif dans le modèle de réactivité du framework depuis sa création. Introduits dans Angular 17 et désormais l'approche par défaut dans Angular 20+, les signals offrent une réactivité granulaire qui élimine les cycles de détection de changements inutiles et s'intègre parfaitement aux applications zoneless.
Les signals sont des primitives réactives synchrones et sans glitch. Un signal computed ne recalcule que lorsque ses dépendances changent et une seule fois par cycle de changement, peu importe combien de fois il est lu.
Comprendre les Primitives Signal dans Angular 20
Le système de signals d'Angular se compose de trois primitives principales : signal(), computed() et effect(). Chacune remplit un rôle distinct dans le graphe réactif.
Un signal inscriptible contient une valeur qui peut être mise à jour avec .set() ou .update(). La lecture du signal retourne la valeur actuelle, et tous les signals computed ou effects qui en dépendent sont notifiés des changements.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
// Writable signal with initial value
count = signal(0);
// Computed signal derives from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() receives current value as argument
this.count.update(c => c + 1);
}
}La différence clé avec le BehaviorSubject de RxJS : les signals sont synchrones et sans glitch. Lorsque count change, doubleCount recalcule exactement une fois, même s'il est lu plusieurs fois dans le même bloc synchrone.
Signals Computed : Évaluation Paresseuse et Mémoïsation
Les signals computed mettent en cache leur résultat jusqu'à ce qu'une dépendance change. Cette mémoïsation est automatique, contrairement à RxJS où shareReplay ou distinctUntilChanged doivent être ajoutés manuellement.
import { Component, signal, computed } from '@angular/core';
interface User {
id: number;
name: string;
purchases: number[];
}
@Component({
selector: 'app-user-stats',
standalone: true,
template: `
<div>
<p>User: {{ user().name }}</p>
<p>Total spent: {{ totalSpent() | currency }}</p>
<p>Average purchase: {{ averagePurchase() | currency }}</p>
</div>
`
})
export class UserStatsComponent {
user = signal<User>({
id: 1,
name: 'Alice',
purchases: [99.99, 149.50, 29.99]
});
// Computed tracks only the properties it reads
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// Depends on another computed signal
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Un signal computed s'évalue paresseusement : la fonction de calcul ne s'exécute que lorsque le signal est lu pour la première fois, pas lors de sa déclaration. Les lectures suivantes retournent la valeur en cache jusqu'à ce qu'une dépendance l'invalide.
Effect : Effets de Bord dans le Graphe Réactif
Les effects exécutent du code en réponse aux changements de signals. Ils s'exécutent de manière asynchrone pendant le cycle de détection de changements, après que toutes les mises à jour synchrones sont terminées.
import { Component, signal, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-theme-sync',
standalone: true,
template: `
<button (click)="toggleTheme()">Toggle Theme</button>
`
})
export class ThemeSyncComponent {
private document = inject(DOCUMENT);
isDarkMode = signal(false);
constructor() {
// Effect syncs signal state to DOM attribute
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect for localStorage persistence
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Les effects suivent les dépendances dynamiquement. Si une branche conditionnelle n'est pas prise, les signals lus uniquement dans cette branche ne sont pas suivis jusqu'à ce que la condition change.
Utiliser effect pour copier des données d'un signal vers un autre indique un problème de conception. Il est préférable d'utiliser computed pour l'état dérivé ou linkedSignal lorsque la valeur dérivée doit être modifiable.
linkedSignal : État Dérivé Modifiable
Introduit dans Angular 19 et stable dans Angular 20, linkedSignal crée un signal modifiable qui se réinitialise lorsque sa source change. Il résout les scénarios où computed est en lecture seule mais où la valeur dérivée nécessite une modification locale.
import { Component, signal, linkedSignal, computed } from '@angular/core';
@Component({
selector: 'app-pagination',
standalone: true,
template: `
<select (change)="pageSize.set(+$any($event.target).value)">
<option [value]="10">10 per page</option>
<option [value]="25">25 per page</option>
<option [value]="50">50 per page</option>
</select>
<p>Page {{ currentPage() }} of {{ totalPages() }}</p>
<button (click)="prevPage()" [disabled]="currentPage() === 1">Prev</button>
<button (click)="nextPage()" [disabled]="currentPage() === totalPages()">Next</button>
`
})
export class PaginationComponent {
totalItems = signal(243);
pageSize = signal(10);
totalPages = computed(() =>
Math.ceil(this.totalItems() / this.pageSize())
);
// linkedSignal resets to 1 when pageSize or totalItems changes
// but allows manual navigation via prevPage/nextPage
currentPage = linkedSignal(() => 1);
prevPage(): void {
this.currentPage.update(p => Math.max(1, p - 1));
}
nextPage(): void {
this.currentPage.update(p => Math.min(this.totalPages(), p + 1));
}
}Sans linkedSignal, ce pattern nécessitait soit un effect écrivant vers un autre signal (anti-pattern), soit une orchestration RxJS complexe.
Prêt à réussir tes entretiens Angular ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
APIs de Composants Basées sur les Signals : input, model, viewChild
Angular 20 promeut les APIs de composants basées sur les signals au statut stable. Celles-ci remplacent les décorateurs par des alternatives réactives qui s'intègrent dans le graphe de signals.
import {
Component,
input,
model,
output,
computed,
viewChild,
ElementRef
} from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div #card class="card" [class.selected]="selected()">
<h3>{{ fullName() }}</h3>
<button (click)="toggle()">{{ selected() ? 'Deselect' : 'Select' }}</button>
</div>
`
})
export class UserCardComponent {
// Signal input replaces @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() creates two-way bindable signal
// Parent uses [(selected)]="parentSignal"
selected = model(false);
// Output emitter for events
cardClicked = output<void>();
// viewChild returns Signal<ElementRef | undefined>
cardElement = viewChild<ElementRef>('card');
// Computed derives from signal inputs
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Les inputs basés sur les signals offrent une meilleure sécurité de typage : input.required() retourne InputSignal<T> tandis que input() retourne InputSignal<T | undefined> à moins qu'une valeur par défaut soit fournie.
Signals vs RxJS : Performance Sous Charge
Les benchmarks dans Angular 21+ démontrent que les signals réduisent la surcharge de détection de changements dans les scénarios intensifs en UI. Sous forte charge avec des mises à jour fréquentes, les signals montrent des profils mémoire plus propres et une utilisation CPU plus faible par rapport aux bindings pilotés par RxJS.
La séparation des préoccupations est claire :
| Cas d'Usage | Approche Recommandée |
|---|---|
| État local du composant | signal() |
| Valeurs UI dérivées | computed() |
| Synchronisation avec APIs externes | effect() |
| Requêtes HTTP | Observable RxJS + toSignal() |
| Flux WebSocket | RxJS avec opérateurs |
| Entrée utilisateur avec debounce | RxJS debounceTime, puis toSignal() |
| Orchestration asynchrone complexe | Pipelines RxJS |
RxJS reste l'outil pour les flux asynchrones, l'annulation et la gestion de la pression. Les signals gèrent l'état synchrone.
Interopérabilité RxJS : toSignal et toObservable
Angular fournit des utilitaires dans @angular/core/rxjs-interop pour faire le pont entre les signals et les observables.
import { Component, signal, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap, distinctUntilChanged } from 'rxjs';
interface SearchResult {
id: number;
title: string;
}
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="query()"
(input)="query.set($any($event.target).value)"
placeholder="Search..."
/>
@if (results(); as items) {
<ul>
@for (item of items; track item.id) {
<li>{{ item.title }}</li>
}
</ul>
}
`
})
export class SearchComponent {
private http = inject(HttpClient);
query = signal('');
// Convert signal to observable for RxJS operators
private query$ = toObservable(this.query);
// RxJS pipeline with debounce, then back to signal
results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => q.length > 2
? this.http.get<SearchResult[]>(`/api/search?q=${q}`)
: []
)
),
{ initialValue: [] as SearchResult[] }
);
}toSignal s'abonne lors de sa création et se désabonne lorsque le composant est détruit. L'option initialValue élimine le type undefined du signal.
Questions d'Entretien : Angular Signals
Les entretiens techniques en 2026 testent fréquemment la compréhension des signals. Voici les patterns qui distinguent les candidats seniors.
Q : Que se passe-t-il si un signal computed lance une erreur ?
L'erreur se propage à tout effect ou computed qui le lit. Angular ne met pas les erreurs en cache, le calcul réessaie lors de la prochaine lecture. Le code en production devrait gérer les erreurs dans la fonction computed.
Q : Peut-on écrire dans un signal à l'intérieur d'un computed ?
Non. Tenter d'appeler .set() ou .update() à l'intérieur d'un computed lance une erreur. Les fonctions computed doivent être pures. Utilisez linkedSignal si une valeur dérivée modifiable est nécessaire.
Q : Comment les effects gèrent-ils les lectures de signals dans les conditions ?
// Dynamic dependency tracking example
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails is only tracked when showDetails is true
console.log('Details:', userDetails().email);
}
});Les effects suivent les dépendances basées sur l'exécution la plus récente. Si showDetails est false, les changements de userDetails ne déclenchent pas l'effect.
Q : Comment fonctionne l'égalité des signals ?
Par défaut, les signals utilisent Object.is pour l'égalité. Pour les objets, cela signifie l'égalité de référence. Une égalité personnalisée peut être fournie :
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);Les candidats qui mentionnent que les signals computed sont mémoïsés et évalués paresseusement démontrent une compréhension au-delà de l'utilisation basique de l'API. Discuter de quand utiliser linkedSignal vs computed montre une familiarité avec les patterns Angular 19+.
Détection de Changements : Signals et Angular Zoneless
Angular 22 utilise par défaut la détection de changements zoneless pour les nouveaux projets. Les signals sont centraux à cela : ils notifient Angular précisément quand l'état change, éliminant le besoin pour Zone.js de patcher les APIs asynchrones.
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-zoneless-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class ZonelessCounterComponent {
count = signal(0);
increment(): void {
// Signal update triggers change detection
// No Zone.js required
this.count.update(c => c + 1);
}
}Avec les signals, Angular suit exactement quels composants dépendent de quel état. Une mise à jour de signal planifie la détection de changements uniquement pour les composants affectés, pas l'arbre entier.
API Resource : Données Asynchrones comme Signals
L'API Resource d'Angular 20 (resource() et rxResource()) charge de manière déclarative les données asynchrones dans des signals, remplaçant les patterns de souscription manuels.
import { Component, signal, computed } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (userResource.isLoading()) {
<p>Loading...</p>
}
@if (userResource.error()) {
<p>Error: {{ userResource.error() }}</p>
}
@if (userResource.value(); as user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}
`
})
export class UserProfileComponent {
private http = inject(HttpClient);
userId = signal(1);
// Declarative async resource
// Refetches when userId changes
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}La ressource suit automatiquement l'état de chargement, les erreurs et la valeur résolue. Elle recharge quand le signal de requête change.
Bonnes Pratiques pour l'Architecture Signal
Structurer les signals dans les applications plus grandes nécessite de la discipline. Ces patterns passent à l'échelle des composants aux services.
Colocaliser les signals liés : Regroupez les signals qui changent ensemble. Un composant de formulaire garde ses signals de champs ensemble, pas dispersés dans les services.
Remonter l'état partagé vers les services : Lorsque plusieurs composants ont besoin du même état, déplacez le signal vers un service injectable :
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
// Expose read-only signal to consumers
readonly cartItems = this.items.asReadonly();
readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly itemCount = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
addItem(item: Omit<CartItem, 'quantity'>): void {
this.items.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...items, { ...item, quantity: 1 }];
});
}
}Préférer computed aux effects : Si une valeur peut être dérivée, utilisez computed. Les effects sont pour les effets de bord qui sortent du graphe réactif : manipulation du DOM, localStorage, analytics, requêtes réseau.
Migrer de RxJS vers les Signals
Les bases de code Angular legacy peuvent migrer de manière incrémentale. Les utilitaires d'interopérabilité RxJS permettent aux signals et aux observables de coexister.
// Before: RxJS-based
@Component({ ... })
export class LegacyComponent {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(
map(u => u?.name ?? 'Guest')
);
}
// After: Signal-based
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}Pour les opérations asynchrones, gardez RxJS pour le pipeline et convertissez à la frontière :
// Hybrid approach
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Préparation aux Questions d'Entretien sur les Angular Signals
- Les signals sont des primitives synchrones ; RxJS gère les flux asynchrones
- Les signals computed sont paresseux et mémoïsés, ne recalculant que lorsque les dépendances changent
- Les effects s'exécutent de manière asynchrone pendant la détection de changements, pas immédiatement
- linkedSignal résout le problème de l'état dérivé modifiable sans effects
- Les inputs signal (
input(),model()) remplacent les décorateurs par des alternatives réactives toSignalettoObservablefont le pont entre les signals et RxJS pour une migration incrémentale- Angular zoneless repose sur les signals pour une détection de changements granulaire
- L'API Resource (
rxResource) gère de manière déclarative l'état asynchrone - Le guide officiel des signals Angular reste la référence autoritaire pour les détails de l'API et les bonnes pratiques
Tu saurais repérer le bug en Angular ?
Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Écrit par
Anthony Fillion-MailletFondateur de SharpSkill
Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.
Mis à jour le 17 septembre 2026
Tags
Partager
Articles similaires

Injection de Dépendances Angular Avancée en 2026 : Providers, Tokens et Questions d'Entretien
Maîtriser l'injection de dépendances Angular avec InjectionToken, les injecteurs hiérarchiques, les modificateurs de résolution et les multi-providers. Inclut des questions d'entretien et des patterns de production.

Syntaxe de flux de contrôle Angular en 2026 : @if, @for, @switch et questions d'entretien
Guide complet sur la syntaxe de flux de contrôle Angular avec @if, @for et @switch. Apprenez à remplacer les directives structurelles par les blocs de contrôle modernes et préparez-vous aux entretiens techniques.

Angular 19 en entretien : Signals, SSR et les questions incontournables
Les questions d'entretien Angular 19 les plus fréquentes : Signals, SSR avec hydratation incrémentale, détection de changement zoneless et nouvelles API réactives.