# 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: `
`
})
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: `
`
})
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: `
`
})
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