📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~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 de computed().


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 un OutputEmitterRef.

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 readonly dans les services, ne jamais appeler httpResource() 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() vs rxResource() : même API, seul le loader diffère. resource() attend une Promise, rxResource() attend un Observable. 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

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.