~2 min de lecture
🧠 Angular Signals — Cheatsheet complet
Valide Angular 17+. Annotations par version pour les APIs récentes.
1. Primitives de base
signal()
import {signal} from '@angular/core';
// Création
const count = signal(0);
const user = signal<User | null>(null);
// Lecture (toujours une fonction)
count() // → 0
user() // → null
// Écriture
count.set(5);
count.update(v => v + 1);
// Mutation (pour objets/tableaux — évite de recréer la référence)
const items = signal<string[]>([]);
items.mutate(arr => arr.push('item')); // ⚠️ Déprécié Angular 17.1+
items.update(arr => [...arr, 'item']); // ✅ Préférer update()
// Égalité personnalisée
const data = signal([], {equal: (a, b) => a.length === b.length});
computed()
import {computed} from '@angular/core';
const firstName = signal('John');
const lastName = signal('Doe');
// Dérivé automatiquement, lazy + memoized
const fullName = computed(() => `${firstName()} ${lastName()}`);
// Computed chaîné
const initials = computed(() =>
fullName().split(' ').map(n => n[0]).join('.')
);
// Égalité personnalisée
const sortedIds = computed(
() => [...ids()].sort(),
{equal: (a, b) => JSON.stringify(a) === JSON.stringify(b)}
);
computed()est read-only. Toute tentative de.set()est une erreur TypeScript.
effect()
import {effect, inject} from '@angular/core';
@Component({...})
export class Counter {
constructor() {
// S'exécute après le premier rendu, puis à chaque changement
effect(() => {
console.log('count =', this.count());
});
// Nettoyage
effect((onCleanup) => {
const timer = setInterval(() => {
}, 1000);
onCleanup(() => clearInterval(timer));
});
// Autoriser les writes dans un effect (usage rare, justifié)
effect(() => {
this.derived.set(this.source() * 2);
}, {allowSignalWrites: true});
}
}
Règle :
effect()pour les side-effects (logs, analytics, sync localStorage). Jamais pour dériver du state — c'est le rôle decomputed().
2. Signals dans les composants
input() — Angular 17.1+
import {input} from '@angular/core';
@Component({...})
export class UserCard {
// Optionnel avec valeur par défaut
readonly size = input<'sm' | 'md' | 'lg'>('md');
// Requis (erreur runtime si absent)
readonly user = input.required<User>();
// Transform
readonly disabled = input(false, {
transform: booleanAttribute // Gère [disabled] et disabled=""
});
readonly maxItems = input(10, {
transform: numberAttribute
});
// Alias
readonly items = input.required<Item[]>({alias: 'data'});
// Utilisation en template
// <app-user-card [user]="currentUser" size="lg" />
}
output() — Angular 17.3+
import {output} from '@angular/core';
@Component({...})
export class UserForm {
readonly saved = output<User>();
readonly cancelled = output<void>();
// Alias
readonly deleted = output<string>({alias: 'userDeleted'});
protected onSave(user: User) {
this.saved.emit(user);
}
}
output()n'est pas un Signal (pas de.()pour lire). C'est unOutputEmitterRef.
model() — Angular 17.2+
import {model} from '@angular/core';
@Component({
template: `<input [value]="value()" (input)="value.set($event.target.value)" />`
})
export class TextInput {
readonly value = model(''); // Optionnel
readonly checked = model.required<boolean>(); // Requis
// Usage parent : [(value)]="myVar"
// Équivaut à : [value]="myVar" (valueChange)="myVar = $event"
}
viewChild() / viewChildren() — Angular 17.3+
import {viewChild, viewChildren, ElementRef} from '@angular/core';
@Component({...})
export class Canvas {
readonly inputEl = viewChild<ElementRef>('inputRef');
// Signal<T> — requis (erreur si introuvable)
readonly canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
// Signal<readonly T[]>
readonly items = viewChildren(ItemCard);
constructor() {
effect(() => {
const el = this.inputEl();
if (el) el.nativeElement.focus();
});
}
}
contentChild() / contentChildren() — Angular 17.3+
import {contentChild, contentChildren} from '@angular/core';
@Component({...})
export class Tabs {
readonly header = contentChild(TabHeader);
readonly panels = contentChildren(TabPanel);
constructor() {
effect(() => {
console.log('panels count:', this.panels().length);
});
}
}
3. linkedSignal() — Angular 19+
Signal writable qui se réinitialise automatiquement quand sa source change.
import {linkedSignal} from '@angular/core';
@Component({...})
export class Pagination {
readonly pageSize = input(10);
// Forme courte : reset complet à chaque changement de source
protected readonly currentPage = linkedSignal(() => 1);
// Forme longue : accès à la valeur précédente
protected readonly selectedItem = linkedSignal<Item | null>({
source: this.items, // Signal source
computation: (items, prev) => {
// Conserver la sélection si l'item existe encore
const prevVal = prev?.value;
return items.find(i => i.id === prevVal?.id) ?? items[0] ?? null;
}
});
}
Cas d'usage typique : sélection dans une liste filtrée, pagination qui revient à 1 quand le filtre change.
4. Interop RxJS
toSignal() — @angular/core/rxjs-interop
import {toSignal} from '@angular/core/rxjs-interop';
import {interval} from 'rxjs';
@Component({...})
export class Dashboard {
private readonly store = inject(Store);
// Valeur initiale obligatoire si l'observable n'émet pas immédiatement
readonly tick = toSignal(interval(1000), {initialValue: 0});
// Depuis un Observable qui émet immédiatement (BehaviorSubject, etc.)
readonly route$ = toSignal(this.router.events);
// Depuis le store NgRx
readonly user = toSignal(this.store.select(selectUser));
// Dans un service (hors contexte d'injection)
readonly data = toSignal(this.http.get('/api/data'), {
injector: inject(Injector)
});
}
toObservable() — @angular/core/rxjs-interop
import {toObservable} from '@angular/core/rxjs-interop';
import {switchMap} from 'rxjs/operators';
@Component({...})
export class SearchComponent {
readonly query = signal('');
// Signal → Observable
readonly query$ = toObservable(this.query);
// Cas d'usage : debounce avant requête
readonly results$ = toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => this.searchService.search(q))
);
}
5. resource() / httpResource() — Angular 19+
resource() — chargement async générique
import {resource, signal} from '@angular/core';
@Component({...})
export class UserDetail {
readonly userId = signal<string | null>(null);
readonly userResource = resource({
params: () => this.userId(), // Dépendance réactive
loader: async ({request: id, abortSignal}) => {
if (!id) return null;
const res = await fetch(`/api/users/${id}`, {signal: abortSignal});
return res.json() as Promise<User>;
}
});
// Signals exposés
// this.userResource.value() → User | null
// this.userResource.isLoading() → boolean
// this.userResource.error() → unknown
// this.userResource.status() → ResourceStatus
// Rechargement manuel
protected reload() {
this.userResource.reload();
}
}
httpResource() — Angular 19.2+
import {httpResource} from '@angular/common/http';
// Dans un service
@Injectable({providedIn: 'root'})
export class UserService {
private readonly apiUrl = '/api/users';
// Simple GET
readonly users = httpResource<User[]>(`${this.apiUrl}`);
// Avec paramètre réactif
userById(id: Signal<string>) {
return httpResource<User>(() => `${this.apiUrl}/${id()}`);
}
// Avec options complètes
searchUsers(query: Signal<string>) {
return httpResource<User[]>(() => ({
url: `${this.apiUrl}/search`,
params: {q: query()},
headers: {'X-Custom': 'value'}
}));
}
}
// Dans le composant
@Component({...})
export class UserList {
private readonly userService = inject(UserService);
readonly users = this.userService.users;
// users.value(), users.isLoading(), users.error()
}
Règle : exposer
readonlydans les services, ne jamais appelerhttpResource()dans une méthode (crée une nouvelle ressource à chaque appel).
rxResource() — Angular 19+
Variante de resource() pour les loaders basés sur un Observable plutôt qu'une Promise.
import {rxResource} from '@angular/core/rxjs-interop';
@Component({...})
export class UserDetail {
private readonly userService = inject(UserService);
readonly userId = signal<string | null>(null);
readonly userResource = rxResource({
params: () => this.userId(),
stream: ({request: id}) => {
if (!id) return of(null);
return this.userService.getById(id); // retourne un Observable
}
});
// Mêmes signals exposés que resource()
// userResource.value(), .isLoading(), .error(), .status()
// userResource.reload()
}
resource()vsrxResource(): même API, seul le loader diffère.resource()attend unePromise,rxResource()attend unObservable. L'Observable est automatiquement unsubscribed si la requête change avant l'émission.
6. Template — référence rapide
<!-- Lecture -->
{{ count() }}
[disabled]="isLoading()"
<!-- Control flow avec signals -->
@if (resource.isLoading()) {
<app-spinner/>
} @else if (resource.error()) {
<p>Erreur : {{ resource.error() }}</p>
} @else {
<ul>
@for (item of resource.value(); track item.id) {
<app-item [item]="item"/>
} @empty {
<p>Aucun résultat</p>
}
</ul>
}
<!-- @let — Angular 18+ -->
@let user = currentUser();
@let name = user?.displayName ?? 'Anonyme';
<h1>{{ name }}</h1>
<!-- Two-way binding avec model() -->
<app-text-input [(value)]="searchQuery"/>
7. Récapitulatif des APIs
| API | Version | Type retour | Writable |
|---|---|---|---|
signal() |
16+ | WritableSignal<T> |
✅ |
computed() |
16+ | Signal<T> |
❌ |
effect() |
16+ | EffectRef |
— |
input() |
17.1+ | InputSignal<T> |
❌ |
input.required() |
17.1+ | InputSignal<T> |
❌ |
output() |
17.3+ | OutputEmitterRef<T> |
— |
model() |
17.2+ | ModelSignal<T> |
✅ |
viewChild() |
17.3+ | Signal<T | undefined> |
❌ |
viewChild.required() |
17.3+ | Signal<T> |
❌ |
viewChildren() |
17.3+ | Signal<readonly T[]> |
❌ |
contentChild() |
17.3+ | Signal<T | undefined> |
❌ |
contentChildren() |
17.3+ | Signal<readonly T[]> |
❌ |
linkedSignal() |
19+ | WritableSignal<T> |
✅ |
resource() |
19+ | ResourceRef<T> |
— |
rxResource() |
19+ | ResourceRef<T> |
— |
httpResource() |
19.2+ | HttpResourceRef<T> |
— |
toSignal() |
16+ | Signal<T> |
❌ |
toObservable() |
16+ | Observable<T> |
— |
📚 Envie de creuser Angular ?
👉🏼 ➡️ Découvre EasyAngularKit