Découvrez les nouveautés d'Angular 20 en quelques minutes

Angular 20 arrive avec plusieurs nouveautés et stabilisation des API: Zoneless, les APIs resource() et httpResource(), un nouveau runner de tests, etc. La vidéo vous donne un aperçu de ces nouveautés.

Abonnez-vous à notre chaîne

Pour profiter des prochaines vidéos sur Angular, abonnez-vous à la nouvelle chaîne YouTube !

Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

Angular Zoneless : La révolution de la détection de changements

Imaginez que vous dirigez un orchestre où chaque musicien doit vous prévenir individuellement quand il a fini de jouer sa partie, plutôt que d'avoir un chef d'orchestre qui surveille tout le monde en permanence. C'est exactement ce que propose le mode Zoneless d'Angular : une approche plus efficace et prévisible de la détection de changements.

Qu'est-ce que Zone.js dans la vraie vie ?

Prenons un exemple concret : vous utilisez une application de messagerie. Quand vous recevez un nouveau message, l'interface doit se mettre à jour pour l'afficher. Zone.js agit comme un surveillant invisible qui observe toutes les actions asynchrones (requêtes HTTP, timers, clics) et déclenche automatiquement une vérification complète de l'interface dès qu'une action se termine.

typescript
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

// Avec Zone.js (comportement automatique)
@Component({
  selector: 'app-messages',
  template: `
    <div>{{ messageCount }} messages</div>
    <button (click)="loadMessages()">Charger</button>
  `
})
export class MessagesComponent {
  messageCount = 0;

  loadMessages() {
    // Zone.js détecte automatiquement cette requête HTTP
    // et déclenche la détection de changements quand elle se termine
    this.http.get('/api/messages').subscribe(messages => {
      this.messageCount = messages.length;
      // L'interface se met à jour automatiquement !
    });
  }
}

Pourquoi vouloir s'en passer ?

Zone.js présente plusieurs inconvénients majeurs :

1. Performance dégradée

Zone.js surveille tout, même quand rien n'a changé. C'est comme avoir un garde qui vérifie toutes les portes d'un bâtiment toutes les 5 secondes, même quand personne n'est entré.

2. Bundle plus lourd

Zone.js ajoute environ 30-40KB à votre application, ce qui impacte le temps de chargement.

3. Débogage complexe

Les stack traces deviennent difficiles à lire car Zone.js "pollue" les appels de fonctions.

4. Incompatibilités

Certaines API modernes (Web Streams, nouvelles promesses) ne sont pas correctement patchées par Zone.js.

Attention aux performances

Zone.js peut déclencher des centaines de cycles de détection inutiles dans une application complexe, impactant directement les Core Web Vitals.

Le mode Zoneless : comment ça marche ?

Le mode Zoneless supprime Zone.js et vous donne le contrôle total sur quand déclencher la détection de changements. C'est plus de travail, mais beaucoup plus efficace.

Les mécanismes de notification

Dans une application Zoneless, Angular ne détecte les changements que dans ces cas précis :

  1. Événements du DOM (click, input, etc.)
  2. Mise à jour de Signals
  3. Appel explicite à markForCheck()
  4. Utilisation d'AsyncPipe
  5. Appel à setInput() sur un ComponentRef
typescript
import { Component, signal, inject, ChangeDetectorRef } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { User } from './user.interface';

@Component({
  selector: 'app-user-list',
  template: `
    <div>
      <h2>Utilisateurs ({{ userCount() }})</h2>
      
      @for (user of users(); track user.id) {
        <div class="user-card">
          <h3>{{ user.name }}</h3>
          <p>{{ user.email }}</p>
        </div>
      }
      
      <button (click)="loadUsers()">Charger les utilisateurs</button>
      <button (click)="loadUsersManual()">Charger (manuel)</button>
    </div>
  `,
  styles: [`
    .user-card {
      border: 1px solid #ddd;
      padding: 1rem;
      margin: 0.5rem 0;
      border-radius: 4px;
    }
  `]
})
export class UserListComponent {
  // Signal pour la liste des utilisateurs - déclenche automatiquement la détection
  private users = signal<User[]>([]);
  
  // Signal calculé pour le nombre d'utilisateurs
  protected userCount = computed(() => this.users().length);
  
  private http = inject(HttpClient);
  private cdr = inject(ChangeDetectorRef);

  /**
   * Charge les utilisateurs en utilisant un Signal
   * La mise à jour du signal déclenche automatiquement la détection de changements
   */
  loadUsers() {
    this.http.get<User[]>('/api/users').subscribe(users => {
      // ✅ La mise à jour du signal déclenche la détection automatiquement
      this.users.set(users);
    });
  }

  /**
   * Charge les utilisateurs avec détection manuelle
   * Nécessaire quand on n'utilise pas de signals
   */
  loadUsersManual() {
    this.http.get<User[]>('/api/users').subscribe(users => {
      // Sans signal, il faut déclencher manuellement
      this.users.set(users);
      // ⚠️ Nécessaire si on modifie des propriétés non-signal
      this.cdr.markForCheck();
    });
  }
}

Configuration du mode Zoneless

Étape 1 : Activer le mode Zoneless (Angular 20+)

typescript
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideZonelessChangeDetection } from '@angular/core';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    // ✅ Active le mode Zoneless
    provideZonelessChangeDetection()
  ]
};
typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));

Étape 2 : Supprimer Zone.js

bash
# Désinstaller Zone.js
npm uninstall zone.js
json
{
  "projects": {
    "your-app": {
      "architect": {
        "build": {
          "options": {
            "polyfills": [
              // ❌ Supprimer cette ligne
              // "zone.js"
            ]
          }
        }
      }
    }
  }
}

Migrer une application existante

Avant : avec Zone.js

typescript
@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>Compteur: {{ count }}</p>
      <button (click)="increment()">+1</button>
      <button (click)="incrementAsync()">+1 (async)</button>
    </div>
  `
})
export class CounterComponent {
  count = 0;

  increment() {
    this.count++; // ✅ Fonctionne avec Zone.js
  }

  incrementAsync() {
    setTimeout(() => {
      this.count++; // ✅ Zone.js détecte le setTimeout
    }, 1000);
  }
}

Après : mode Zoneless avec Signals

typescript
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>Compteur: {{ count() }}</p>
      <button (click)="increment()">+1</button>
      <button (click)="incrementAsync()">+1 (async)</button>
    </div>
  `
})
export class CounterComponent {
  // ✅ Utilisation d'un signal
  protected count = signal(0);

  increment() {
    // ✅ La mise à jour du signal déclenche la détection
    this.count.update(value => value + 1);
  }

  incrementAsync() {
    setTimeout(() => {
      // ✅ Fonctionne aussi dans les callbacks asynchrones
      this.count.update(value => value + 1);
    }, 1000);
  }
}

Bonnes pratiques en mode Zoneless

1. Privilégier OnPush partout

typescript
@Component({
  selector: 'app-user-card',
  changeDetection: ChangeDetectionStrategy.OnPush, // ✅ Recommandé
  template: `
    <div class="card">
      <h3>{{ user().name }}</h3>
      <p>{{ user().email }}</p>
    </div>
  `
})
export class UserCardComponent {
  user = input.required<User>();
}

2. Utiliser les Signals pour l'état

typescript
export class UserService {
  // ✅ Signal pour l'état global
  private users = signal<User[]>([]);
  
  // ✅ Signal en lecture seule pour les composants
  readonly usersReadonly = this.users.asReadonly();

  /**
   * Charge les utilisateurs depuis l'API
   * Met à jour le signal qui notifie automatiquement tous les composants abonnés
   */
  loadUsers() {
    return this.http.get<User[]>('/api/users').pipe(
      tap(users => this.users.set(users))
    );
  }

  /**
   * Ajoute un nouvel utilisateur
   * @param user - L'utilisateur à ajouter
   */
  addUser(user: User) {
    this.users.update(current => [...current, user]);
  }
}

3. Gérer les tests

typescript
describe('UserListComponent', () => {
  let component: UserListComponent;
  let fixture: ComponentFixture<UserListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserListComponent],
      providers: [
        // ✅ Activer Zoneless dans les tests aussi
        provideZonelessChangeDetection()
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(UserListComponent);
    component = fixture.componentInstance;
  });

  it('should update user count when users are loaded', async () => {
    // ✅ Utiliser whenStable() au lieu de detectChanges()
    component.loadUsers();
    await fixture.whenStable();
    
    expect(component.userCount()).toBe(3);
  });
});

Conseil pour les tests

En mode Zoneless, préférez fixture.whenStable() à fixture.detectChanges() pour attendre que toutes les opérations asynchrones se terminent.