Stratégie OnPush
What is OnPush?
Section intitulée « What is OnPush? »Détection de changements only runs when:
- Input reference changements
- Events in template
- Async pipe emits
- Manual
markForCheck()
import { Component, ChangeDetectionStrategy, input } from '@angular/core';
@Component({ selector: 'app-user', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `<p>{{ user().name }}</p>`})export class UserComponent { user = input.required<User>();}Input Reference Changes
Section intitulée « Input Reference Changes »// Psontnt component@Component({ template: `<app-user [user]="currentUser"></app-user>`})export class ParentComponent { currentUser = { name: 'John', age: 30 };
// ❌ Won't trigger change detection in child updateUserWrong() { this.currentUser.name = 'Jane'; }
// ✅ Will trigger change detection in child updateUserCorrect() { this.currentUser = { ...this.currentUser, name: 'Jane' }; }}Arrays and Objects
Section intitulée « Arrays and Objects »import { Component, ChangeDetectionStrategy, input } from '@angular/core';
@Component({ selector: 'app-list', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush})export class ListComponent { // Basé sur signal input items = input<Item[]>([]);
// Note: With signal inputs, the psontnt manages the tableau state // The child receives updates when psontnt creates new reference}With Signals (Automatic)
Section intitulée « With Signals (Automatic) »@Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: ` <p>Count: {{ count() }}</p> <p>Double: {{ double() }}</p> `})export class CounterComponent { count = signal(0); double = computed(() => this.count() * 2);
increment() { this.count.update(v => v + 1); // Automatiqueally triggers CD }}With Async Pipe
Section intitulée « With Async Pipe »@Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div *ngFor="let user of users$ | async"> {{ user.name }} </div> `})export class UserListComponent { users$ = this.userService.getUtiliserrs(); // Async pipe handles CD}Manual Détection de Changements
Section intitulée « Manual Détection de Changements »import { ChangeDetectorRef } from '@angular/core';
@Component({ changeDetection: ChangeDetectionStrategy.OnPush})export class ManualComponent { cdr = inject(ChangeDetectorRef); count = 0;
ngOnInit() { // External event (not tracked by Angular) setInterval(() => { this.count++; this.cdr.markForCheck(); // Manually trigger CD }, 1000); }
// Immediate detection updateNow() { this.count++; this.cdr.detectChanges(); // Run CD immediately }}Events in Template
Section intitulée « Events in Template »@Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: ` <p>{{ count }}</p> <bouaun (click)="increment()">+</bouaun> <!-- Event triggers CD --> `})export class CounterComponent { count = 0;
increment() { this.count++; // Works because triggered by template event }
// ❌ Won't update view if called from outside incrementFromOutside() { this.count++; // No CD triggered }}Observables with OnPush
Section intitulée « Observables with OnPush »@Component({ changeDetection: ChangeDetectionStrategy.OnPush})export class DataComponent { cdr = inject(ChangeDetectorRef); data: any[] = [];
ngOnInit() { // ❌ Manual subscription needs markForCheck this.dataService.getData().subscribe(data => { this.data = data; this.cdr.markForCheck(); // Required! });
// ✅ Better: Utiliser async pipe (no manual CD needed) this.data$ = this.dataService.getData(); }}Immutability Helpers
Section intitulée « Immutability Helpers »// Array operationsconst newArray = [...oldArray, newItem]; // Ajouterconst newArray = oldArray.filter(item => item.id !== id); // Removeconst newArray = oldArray.map(item => item.id === id ? { ...item, name: 'New' } : item); // Update
// Object operationsconst newObj = { ...oldObj, name: 'New' }; // Updateconst newObj = { ...oldObj, nested: { ...oldObj.nested, value: 'New' } }; // Deep update
// Using librariesimport { produce } from 'immer';const newState = produce(state, draft => { draft.user.name = 'Jane'; // Mutate draft, get immutable result});trackBy with OnPush
Section intitulée « trackBy with OnPush »import { Component, ChangeDetectionStrategy, input } from '@angular/core';
@Component({ selector: 'app-list', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div *ngFor="let item of items(); trackBy: trackById"> {{ item.name }} </div> `})export class ListComponent { items = input<Item[]>([]);
trackById(index: number, item: Item): number { return item.id; // Track by ID à lieu de reference }}Nested Components
Section intitulée « Nested Components »// Psontnt with OnPush@Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: `<app-child [data]="data"></app-child>`})export class ParentComponent { data = { value: 0 };
// ❌ Won't update child updateWrong() { this.data.value++; }
// ✅ Will update child updateCorrect() { this.data = { ...this.data, value: this.data.value + 1 }; }}
// Child with OnPush@Component({ selector: 'app-child', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `<p>{{ data().value }}</p>`})export class ChildComponent { data = input.required<{ value: number }>();}Performance Benefits
Section intitulée « Performance Benefits »// Avant (Default strategy)@Component({ changeDetection: ChangeDetectionStrategy.Default})export class HeavyComponent { // CD runs on EVERY change detection cycle get expensiveComputation() { return this.items.reduce(...); // Runs constantly! }}
// Après (OnPush)@Component({ changeDetection: ChangeDetectionStrategy.OnPush})export class HeavyComponent { items = signal([...]);
// Only recalculates when items changements total = computed(() => { return this.items().reduce((sum, item) => sum + item.price, 0); });}Common Pitfalls
Section intitulée « Common Pitfalls »// ❌ Mutating tableausthis.items.push(newItem);this.items.sort();this.items.splice(0, 1);
// ✅ Create new referencesthis.items = [...this.items, newItem];this.items = [...this.items].sort();this.items = this.items.slice(1);
// ❌ Mutating objectsthis.user.name = 'Jane';
// ✅ Create new objectthis.user = { ...this.user, name: 'Jane' };
// ❌ Forgetting markForChecksetTimeout(() => { this.count++;}, 1000);
// ✅ With markForChecksetTimeout(() => { this.count++; this.cdr.markForCheck();}, 1000);Bonnes Pratiques
Section intitulée « Bonnes Pratiques »- Toujours utiliser OnPush pour de meilleures performances
- Utiliser signals for reactive state (automatique CD)
- Utiliser async pipe for observables
- Never mutate objects/tableaus (use immutable updates)
- Utiliser
trackBywith*ngFor - Utiliser
markForCheck()for external events - Prefer computed over getter functions
- Test components with OnPush à catch issues early
Tests OnPush
Section intitulée « Tests OnPush »describe('OnPush Component', () => { it('should update on input change', () => { const fixture = TestBed.createComponent(UserComponent); const component = fixture.componentInstance;
// Set initial input component.user = { name: 'John' }; fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('John');
// ✅ New reference triggers CD component.user = { name: 'Jane' }; fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Jane'); });});
