# Angular 18 : Signals et nouvelles fonctionnalités > Découvrez les Signals d'Angular 18, la détection de changement zoneless et les nouvelles APIs signal-based pour créer des applications plus performantes. - Published: 2026-01-18 - Updated: 2026-03-31 - Author: SharpSkill - Tags: angular 18, angular signals, zoneless, signal inputs, réactivité - Reading time: 12 min --- Angular 18 marque un tournant majeur dans l'évolution du framework avec la stabilisation des Signals. Cette nouvelle primitive réactive transforme fondamentalement la façon de construire des composants Angular, offrant une alternative moderne aux décorateurs traditionnels tout en préparant le terrain pour une détection de changement sans Zone.js. > **Ce que vous allez apprendre** > > Les APIs signal-based d'Angular 18 : input(), model(), viewChild(), et la configuration zoneless pour des applications plus légères et performantes. ## Comprendre les Signals dans Angular 18 Les Signals représentent une nouvelle approche de la réactivité dans Angular. Contrairement aux décorateurs classiques comme `@Input()` qui reposent sur la détection de changement de Zone.js, les Signals offrent une réactivité fine et explicite. Chaque Signal encapsule une valeur et notifie automatiquement les consommateurs lorsque cette valeur change. Cette approche présente plusieurs avantages : une meilleure performance grâce à des mises à jour ciblées, une intégration native avec les fonctions `computed()` et `effect()`, et une préparation à l'avenir zoneless d'Angular. ```typescript // signals-basics.component.ts // Démonstration des concepts fondamentaux des Signals import { Component, signal, computed, effect } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: `

Compteur : {{ count() }}

Double : {{ doubleCount() }}

` }) export class CounterComponent { // Signal writable - la valeur peut être modifiée count = signal(0); // Signal computed - dérivé automatiquement de count // Se recalcule uniquement quand count change doubleCount = computed(() => this.count() * 2); constructor() { // Effect - exécuté à chaque changement de count // Utile pour les side effects (logs, API calls, etc.) effect(() => { console.log(`Nouvelle valeur du compteur : ${this.count()}`); }); } increment() { // update() permet de modifier basé sur la valeur précédente this.count.update(value => value + 1); } decrement() { this.count.update(value => value - 1); } reset() { // set() remplace directement la valeur this.count.set(0); } } ``` Les Signals fonctionnent comme des conteneurs réactifs : `signal()` crée un Signal modifiable, `computed()` dérive des valeurs calculées, et `effect()` permet d'exécuter des actions en réponse aux changements. ## Signal Inputs avec input() La fonction `input()` remplace le décorateur `@Input()` traditionnel. Elle retourne un `InputSignal` en lecture seule, garantissant que les données circulent toujours du parent vers l'enfant sans modification accidentelle. ```typescript // book-card.component.ts // Composant utilisant les signal inputs import { Component, input, computed } from '@angular/core'; interface Book { id: string; title: string; author: string; price: number; discountPercent?: number; } @Component({ selector: 'app-book-card', standalone: true, template: `

{{ book().title }}

Par {{ book().author }}

@if (hasDiscount()) {

{{ book().price }} € {{ discountedPrice() }} €

} @else {

{{ book().price }} €

} @if (featured()) { Coup de cœur }
` }) export class BookCardComponent { // Input requis - le template ne compile pas sans cette prop book = input.required(); // Input optionnel avec valeur par défaut featured = input(false); // Computed basé sur l'input - recalculé automatiquement hasDiscount = computed(() => { const discount = this.book().discountPercent; return discount !== undefined && discount > 0; }); // Calcul du prix réduit discountedPrice = computed(() => { const { price, discountPercent } = this.book(); if (!discountPercent) return price; return (price * (100 - discountPercent) / 100).toFixed(2); }); } ``` L'utilisation dans un template parent reste similaire, mais avec la garantie de type et la réactivité des Signals : ```typescript // book-list.component.ts // Composant parent utilisant book-card import { Component, signal } from '@angular/core'; import { BookCardComponent } from './book-card.component'; @Component({ selector: 'app-book-list', standalone: true, imports: [BookCardComponent], template: `
@for (book of books(); track book.id) { }
` }) export class BookListComponent { books = signal([ { id: '1', title: 'Clean Code', author: 'Robert C. Martin', price: 35 }, { id: '2', title: 'The Pragmatic Programmer', author: 'David Thomas', price: 42, discountPercent: 15 } ]); featuredBookId = signal('1'); } ``` La différence majeure avec `@Input()` : les signal inputs sont en lecture seule. Impossible de modifier `this.book.set()` depuis le composant enfant, ce qui renforce le flux de données unidirectionnel. ## Two-Way Binding avec model() Pour les cas nécessitant une synchronisation bidirectionnelle, Angular 18 introduit `model()`. Cette fonction crée un Signal modifiable qui propage automatiquement les changements vers le composant parent. ```typescript // search-input.component.ts // Composant avec binding bidirectionnel via model() import { Component, model, output, computed } from '@angular/core'; @Component({ selector: 'app-search-input', standalone: true, template: `
@if (query().length > 0) { } {{ charCount() }} caractères
` }) export class SearchInputComponent { // model() crée un Signal bidirectionnel // Les modifications se propagent au parent query = model(''); // Input classique pour la configuration placeholder = model('Rechercher...'); // Output pour les événements supplémentaires searchSubmitted = output(); // Computed basé sur le model charCount = computed(() => this.query().length); onInput(event: Event) { const value = (event.target as HTMLInputElement).value; // Mise à jour du model - propage au parent this.query.set(value); } clear() { this.query.set(''); } submit() { if (this.query().length > 0) { this.searchSubmitted.emit(this.query()); } } } ``` Le parent utilise la syntaxe banana-in-a-box `[()]` pour le binding bidirectionnel : ```typescript // app.component.ts // Utilisation du two-way binding avec model() import { Component, signal, effect } from '@angular/core'; import { SearchInputComponent } from './search-input.component'; @Component({ selector: 'app-root', standalone: true, imports: [SearchInputComponent], template: `

Recherche en cours : {{ searchTerm() }}

@for (result of filteredResults(); track result.id) {
{{ result.name }}
}
` }) export class AppComponent { // Signal local synchronisé avec le composant enfant searchTerm = signal(''); results = signal([ { id: 1, name: 'Angular 18' }, { id: 2, name: 'React 19' }, { id: 3, name: 'Vue 3' } ]); // Filtrage réactif basé sur searchTerm filteredResults = computed(() => { const term = this.searchTerm().toLowerCase(); if (!term) return this.results(); return this.results().filter(r => r.name.toLowerCase().includes(term) ); }); } ``` > **model() vs input()** > > Utilisez `input()` pour les données en lecture seule (parent → enfant). Utilisez `model()` quand le composant enfant doit pouvoir modifier la valeur (bidirectionnel). ## Signal Queries avec viewChild() et contentChild() Les fonctions `viewChild()`, `viewChildren()`, `contentChild()` et `contentChildren()` remplacent les décorateurs correspondants. Elles retournent des Signals, éliminant le besoin des lifecycle hooks comme `ngAfterViewInit`. ```typescript // form-container.component.ts // Démonstration des signal queries import { Component, viewChild, viewChildren, ElementRef, effect, signal } from '@angular/core'; import { FormFieldComponent } from './form-field.component'; @Component({ selector: 'app-form-container', standalone: true, imports: [FormFieldComponent], template: `
` }) export class FormContainerComponent { // viewChild retourne un Signal formElement = viewChild('formElement'); // viewChild.required garantit que l'élément existe firstInput = viewChild.required>('firstInput'); // Query sur un composant - retourne le composant lui-même firstFormField = viewChild(FormFieldComponent); // viewChildren pour plusieurs éléments allFormFields = viewChildren(FormFieldComponent); constructor() { // Effect remplace ngAfterViewInit pour les queries effect(() => { // Le Signal est automatiquement résolu const input = this.firstInput(); console.log('Premier input disponible:', input.nativeElement); }); // Réagir aux changements de la liste effect(() => { const fields = this.allFormFields(); console.log(`${fields.length} champs de formulaire trouvés`); }); } focusFirst() { // Accès direct via le Signal this.firstInput().nativeElement.focus(); } onSubmit(event: Event) { event.preventDefault(); // Accéder au formulaire const form = this.formElement(); if (form) { console.log('Formulaire soumis'); } } } ``` Pour projeter du contenu et y accéder, `contentChild()` fonctionne de manière similaire : ```typescript // card.component.ts // Utilisation de contentChild pour le contenu projeté import { Component, contentChild, contentChildren, TemplateRef } from '@angular/core'; @Component({ selector: 'app-card', standalone: true, template: `
@if (hasFooter()) {
}
` }) export class CardComponent { // Détecter si un footer a été projeté footerContent = contentChild('[card-footer]'); // Computed pour vérifier la présence du footer hasFooter = computed(() => this.footerContent() !== undefined); } ``` ## Détection de changement Zoneless Angular 18 introduit la détection de changement sans Zone.js en mode expérimental. Cette fonctionnalité réduit la taille du bundle d'environ 13 KB et améliore les performances en éliminant les monkey-patches sur les APIs asynchrones du navigateur. ```typescript // main.ts // Configuration de l'application en mode zoneless import { bootstrapApplication } from '@angular/platform-browser'; import { provideExperimentalZonelessChangeDetection } from '@angular/core'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [ // Active la détection zoneless expérimentale provideExperimentalZonelessChangeDetection() ] }); ``` La configuration dans `angular.json` doit également être mise à jour pour retirer Zone.js : ```json { "projects": { "my-app": { "architect": { "build": { "options": { "polyfills": [] } } } } } } ``` En mode zoneless, la détection de changement se déclenche automatiquement dans ces cas : mise à jour d'un Signal, appel à `markForCheck()`, réception d'une nouvelle valeur via AsyncPipe, ou attachement/détachement d'un composant. ```typescript // zoneless-counter.component.ts // Composant optimisé pour le mode zoneless import { Component, signal, ChangeDetectionStrategy, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { toSignal } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-zoneless-counter', standalone: true, // OnPush recommandé pour zoneless changeDetection: ChangeDetectionStrategy.OnPush, template: `

Compteur : {{ count() }}

@if (loading()) {

Chargement...

} @if (data()) {
{{ data() | json }}
}
` }) export class ZonelessCounterComponent { private http = inject(HttpClient); count = signal(0); loading = signal(false); data = signal(null); increment() { // La mise à jour du Signal déclenche la détection this.count.update(c => c + 1); } async fetchData() { this.loading.set(true); try { // Les Signals garantissent la mise à jour de la vue const response = await fetch('/api/data'); const json = await response.json(); this.data.set(json); } finally { this.loading.set(false); } } } ``` > **Compatibilité Zoneless** > > Les composants utilisant `ChangeDetectionStrategy.OnPush` et les Signals sont généralement compatibles avec le mode zoneless. Évitez les modifications directes de propriétés qui ne sont pas des Signals. ## Migration des composants existants La migration vers les APIs signal-based peut se faire progressivement. Voici un exemple de refactoring d'un composant traditionnel : ```typescript // AVANT : Composant avec décorateurs classiques // user-profile-legacy.component.ts import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-user-profile-legacy', template: `

{{ user.name }}

{{ user.email }}

` }) export class UserProfileLegacyComponent implements AfterViewInit { @Input() user!: { name: string; email: string }; @ViewChild('container') container!: ElementRef; ngAfterViewInit() { console.log('Container ready:', this.container.nativeElement); } } ``` ```typescript // APRÈS : Composant migré vers les Signals // user-profile.component.ts import { Component, input, viewChild, ElementRef, effect } from '@angular/core'; interface User { name: string; email: string; } @Component({ selector: 'app-user-profile', standalone: true, template: `

{{ user().name }}

{{ user().email }}

` }) export class UserProfileComponent { // input.required remplace @Input() avec ! user = input.required(); // viewChild.required remplace @ViewChild avec ! container = viewChild.required('container'); constructor() { // effect remplace ngAfterViewInit pour les queries effect(() => { console.log('Container ready:', this.container().nativeElement); }); } } ``` Les avantages de cette migration : typage plus strict, réactivité automatique, moins de code boilerplate, et compatibilité avec le mode zoneless. ## Bonnes pratiques avec les Signals Quelques recommandations pour tirer le meilleur parti des Signals dans Angular 18 : ```typescript // best-practices.component.ts // Exemple de bonnes pratiques avec les Signals import { Component, signal, computed, effect, untracked, ChangeDetectionStrategy } from '@angular/core'; interface Product { id: string; name: string; price: number; quantity: number; } @Component({ selector: 'app-cart', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `

Panier ({{ itemCount() }} articles)

@for (item of items(); track item.id) {
{{ item.name }} {{ item.quantity }} × {{ item.price }} €
}
Total : {{ total() }} €
` }) export class CartComponent { // Signal pour les données mutables items = signal([]); // Computed pour les valeurs dérivées - évite les recalculs inutiles itemCount = computed(() => this.items().length); total = computed(() => this.items().reduce((sum, item) => sum + item.price * item.quantity, 0) ); constructor() { // Effect pour les side effects (analytics, persistence) effect(() => { const currentItems = this.items(); // untracked évite de créer une dépendance untracked(() => { localStorage.setItem('cart', JSON.stringify(currentItems)); }); }); } addItem(product: Product) { // update() pour les modifications basées sur l'état précédent this.items.update(current => { const existing = current.find(i => i.id === product.id); if (existing) { return current.map(i => i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i ); } return [...current, { ...product, quantity: 1 }]; }); } removeItem(id: string) { this.items.update(current => current.filter(i => i.id !== id)); } } ``` Points clés à retenir : - Utiliser `computed()` pour les valeurs dérivées plutôt que de les recalculer dans le template - Préférer `update()` à `set()` quand la nouvelle valeur dépend de l'ancienne - Utiliser `untracked()` dans les effects pour éviter les dépendances circulaires - Toujours spécifier `track` dans les boucles `@for` pour optimiser le rendu ## Conclusion Angular 18 pose les fondations d'un futur sans Zone.js grâce aux Signals. Les points essentiels à retenir : - ✅ **input()** remplace `@Input()` avec un typage plus strict et une lecture seule garantie - ✅ **model()** permet le two-way binding réactif entre parent et enfant - ✅ **viewChild()** et **contentChild()** éliminent le besoin de lifecycle hooks - ✅ **Zoneless** réduit le bundle et améliore les performances - ✅ **computed()** et **effect()** complètent l'écosystème réactif - ✅ Migration progressive possible composant par composant L'adoption des Signals prépare les applications Angular pour les futures versions où le mode zoneless deviendra la norme. Cette transition représente un investissement judicieux pour la maintenabilité et les performances à long terme. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/angular/angular-18-signals-nouvelles-fonctionnalites