Replay

Apprendre Angular en 1h

Je me donne un objectif: vous faire découvrir et apprendre Angular en 1 heure: composant, syntaxe dans les templates, les directives, les signaux, les routeurs, les services, l'injection de dépendances, les observables et les requêtes HTTP. Le nécessaire pour faire une application Angular !.

Skip to content

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

signal(), computed() et effect()

Pourquoi Angular contient désormais la notion des signaux ?

  1. Meilleures performances d'exécution : Les signaux permettent de réduire le nombre de calculs effectués lors de la détection des changements dans une application Angular. Cela se traduit par de meilleures performances d'exécution, ce qui signifie que l'application fonctionnera plus rapidement et de manière plus fluide pour les utilisateurs.

  2. Modèle mental plus simple pour la réactivité : Les signaux offrent un modèle mental plus simple pour comprendre et gérer la réactivité dans une application. Ils permettent de clarifier les dépendances de la vue (ce qui influence quoi) et le flux des données à travers l'application, ce qui facilite la compréhension du fonctionnement de l'application.

  3. Réactivité fine-grainée : Avec les signaux, il est possible d'effectuer une réactivité fine-grainée. Cela signifie que dans les versions futures d'Angular, il sera possible de vérifier les changements uniquement dans les composants concernés, ce qui améliorera les performances et l'efficacité de l'application.

  4. Réduction de la dépendance à Zone.js : Les signaux permettent de rendre Zone.js facultatif dans les versions futures d'Angular. Zone.js est une bibliothèque utilisée par Angular pour détecter les changements et exécuter les tâches asynchrones. En utilisant les signaux, Angular peut être notifié directement lorsque le modèle de données a changé, ce qui réduit la dépendance à Zone.js et peut potentiellement améliorer les performances.

  5. Propriétés calculées sans pénalité de recomputation : Les signaux permettent de définir des propriétés calculées qui sont mises à jour automatiquement lorsque les valeurs dont elles dépendent changent. Cela évite de devoir recalculer ces propriétés à chaque cycle de détection des changements, ce qui améliore les performances et l'efficacité de l'application.

  6. Meilleure interoperabilité avec RxJS : Les signaux facilitent l'interopérabilité avec RxJS, une bibliothèque de programmation réactive très populaire. Cela permet d'utiliser les fonctionnalités avancées de RxJS en combinaison avec les signaux, offrant ainsi une plus grande flexibilité et des possibilités étendues de gestion des données réactives dans l'application.

Exemple complet

Imaginons que vous développez une application de gestion des utilisateurs pour une entreprise. Vous avez besoin d'afficher une liste d'utilisateurs, de pouvoir les rechercher en temps réel, et d'envoyer des notifications lorsque certains événements se produisent. Les signaux d'Angular sont parfaits pour cette tâche !

ts
import { Component, signal, computed, effect } from '@angular/core';
import { UserService } from './user.service';
import { User } from './user.interface';

@Component({
  selector: 'app-user-list',
  template: `
    <div class="search-container">
      <input 
        type="text" 
        [ngModel]="searchQuery()" 
        (ngModelChange)="searchQuery.set($event)"
        placeholder="Rechercher un utilisateur..."
      >
    </div>

    <div class="users-container">
      @if (filteredUsers().length > 0) {
        @for (user of filteredUsers(); track user.id) {
          <div class="user-card">
            <h3>{{ user.name }}</h3>
            <p>{{ user.email }}</p>
            <span class="role-badge">{{ user.role }}</span>
          </div>
        }
      } @else {
        <p>Aucun utilisateur trouvé</p>
      }
    </div>

    <div class="stats">
      Nombre d'administrateurs: {{ adminCount() }}
    </div>
  `
})
export class UserListComponent {
  private userService = inject(UserService);

  // Signal principal contenant notre liste d'utilisateurs
  users = signal<User[]>([]);
  
  // Signal pour la recherche
  searchQuery = signal('');
  
  // Signal calculé pour filtrer les utilisateurs
  filteredUsers = computed(() => {
    const query = this.searchQuery().toLowerCase();
    return this.users().filter(user => 
      user.name.toLowerCase().includes(query) || 
      user.email.toLowerCase().includes(query)
    );
  });
  
  // Signal calculé pour compter les administrateurs
  adminCount = computed(() => {
    return this.users().filter(user => user.role === 'admin').length;
  });
  
  constructor() {
    // Chargement initial des utilisateurs
    // D'autres meilleures pratiques pour faire ça :
    // - Utiliser rxResource (v19+)
    // - Transformer en signal avec toSignal
    // > Nous laissons comme ça pour l'instant pour la compréhension ;)
    this.userService.getUsers().subscribe(
      users => this.users.set(users)
    );
    
    // Effet pour surveiller les administrateurs inactifs
    effect(() => {
      const inactiveAdmins = this.users().filter(user => {
        const lastActive = new Date(user.lastActive);
        const oneHourAgo = new Date(Date.now() - 3600000);
        return user.role === 'admin' && lastActive < oneHourAgo;
      });
      
      if (inactiveAdmins.length > 0) {
        this.userService.notifyAdmin(
          `${inactiveAdmins.length} administrateurs sont inactifs depuis plus d'une heure`
        );
      }
    });
  }
}
ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { User } from './user.interface';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private http = inject(HttpClient);
  
  // Simulons une API
  getUsers() {
    return this.http.get<User[]>('/api/users');
  }
  
  notifyAdmin(message: string) {
    console.log(`Notification admin: ${message}`);
  }
}
ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
  lastActive: Date;
}

signal()

Dans notre exemple, nous utilisons deux signaux principaux :

typescript
users = signal<User[]>([]);
searchQuery = signal('');

// Modifier un signal
searchQuery.set('John');

// Lire le signal
searchQuery()

// Modifier un signal qui contient un tableau
// On peut faire:
users.set([...users(), { id: 4, name: 'John Doe', email: '[email protected]', role: 'admin', lastActive: new Date() }]);

Un signal est une représentation d'une information qui peut changer au fil du temps. Pour mieux comprendre ce concept, prenons un exemple concret de la vie réelle.

Imaginez que vous avez un indicateur lumineux sur votre téléphone portable pour vous informer de la quantité de batterie restante. Ce voyant lumineux peut être considéré comme un signal de la batterie.

Lorsque votre téléphone est complètement chargé, le signal de la batterie peut être vert pour indiquer que la batterie est pleine. À mesure que vous utilisez votre téléphone et que la charge diminue, le signal peut passer au jaune pour indiquer un niveau de batterie moyen. Enfin, lorsque votre batterie est presque épuisée, le signal peut devenir rouge pour vous avertir que vous devez recharger votre téléphone.

Dans cet exemple, le signal de la batterie est une représentation visuelle de l'état de la batterie. Il change en fonction du niveau de charge de la batterie. Vous pouvez considérer ce signal comme une valeur réactive qui évolue en fonction d'un état spécifique.

De manière plus générale, dans le développement logiciel, un signal est utilisé pour représenter des informations qui peuvent changer au fil du temps, comme l'état d'un bouton, le nombre de likes sur un article, ou la position d'un objet sur une carte. Les signaux sont utilisés pour suivre ces changements et réagir en conséquence dans une application.

Ici, le signal users est comme une boîte qui contient notre liste d'utilisateurs. Imaginez-le comme un tableau d'affichage dans une entreprise : quand on met à jour la liste des employés, tout le monde peut voir les changements immédiatement.

Pourquoi utiliser un signal ici ?

Les signaux nous permettent de mettre à jour notre liste d'utilisateurs de manière réactive. Dès que nous modifions la liste (avec users.set()), tous les éléments qui en dépendent (comme le filtrage ou le compteur d'administrateurs) sont automatiquement mis à jour.

Signal en lecture seule

Vous pouvez rendre un signal en lecture seule, ce qui signifie que vous ne pouvez pas modifier directement sa valeur en dehors de la fonction set. Cela garantit que les valeurs des signaux sont gérées de manière cohérente et que les mises à jour sont effectuées de manière contrôlée. Utilisez la fonction asReadOnly pour obtenir une version en lecture seule du signal.

ts
const isTaskCompleted = signal(false);
const readOnlyTaskCompleted = isTaskCompleted.asReadOnly();
readOnlyTaskCompleted.set(true); // erreur de compilation

Il est recommandé d'utiliser les signaux en lecture seule dans les composants, et de faire la modification dans le service car c'est la responsabilité du service de gérer les données. De cette manière, on mettra un contrôle et une accessibilité restrictive sur les signaux utilisés dans les composants.

computed()

typescript
filteredUsers = computed(() => {
  const query = this.searchQuery().toLowerCase();
  return this.users().filter(user => 
    user.name.toLowerCase().includes(query) || 
    user.email.toLowerCase().includes(query)
  );
});

Imaginez computed comme un assistant qui surveille vos signaux et recalcule automatiquement un résultat quand nécessaire.

Comment computed() fonctionne ?

Remarquez que la fonction contient le signal searchQuery() et le signal users(). Donc, dès que l'un de ces signaux change, la fonction est réexécutée.

Performance

Les valeurs calculées sont mises en cache ! Si vous faites plusieurs appels à filteredUsers() sans que users ou searchQuery ne changent, le calcul n'est pas refait.

effect()

typescript
effect(() => {
  const inactiveAdmins = this.users().filter(user => {
    const lastActive = new Date(user.lastActive);
    const oneHourAgo = new Date(Date.now() - 3600000);
    return user.role === 'admin' && lastActive < oneHourAgo;
  });
  
  if (inactiveAdmins.length > 0) {
    this.userService.notifyAdmin(
      `${inactiveAdmins.length} administrateurs sont inactifs depuis plus d'une heure`
    );
  }
});

L'effet est comme un gardien qui surveille constamment une situation et réagit quand quelque chose d'important se produit. Dans notre cas, il vérifie si des administrateurs sont inactifs depuis trop longtemps et envoie une notification si nécessaire.

Comment effect() fonctionne ?

L'effet est exécuté chaque fois que les signaux dont il dépend changent. Finalement, ça ressemble beaucoup à computed() mais avec des effets secondaires. Il n'y a pas de valeur de retour et rien n'est mis en cache.

effect dans le constructeur

Par défaut, vous ne pouvez créer un effect() que dans un contexte d'injection (où vous avez accès à la fonction inject). La façon la plus simple est d'appeler effect dans le constructeur d'un composant, d'une directive ou d'un service :

typescript
@Component({...})
export class UserListComponent {
  readonly users = signal<User[]>([]);
  
  constructor() {
    // Enregistrement d'un nouvel effet
    effect(() => {
      console.log(`Nombre d'utilisateurs : ${this.users().length}`);
    });
  }
}

Vous pouvez également assigner l'effet à un champ (ce qui lui donne aussi un nom descriptif) :

typescript
@Component({...})
export class UserListComponent {
  readonly users = signal<User[]>([]);
  
  private loggingEffect = effect(() => {
    console.log(`Nombre d'utilisateurs : ${this.users().length}`);
  });
}

Pour créer un effet en dehors du constructeur, vous devez passer un Injector à effect via ses options :

typescript
@Component({...})
export class UserListComponent {
  readonly users = signal<User[]>([]);
  
  constructor(private injector: Injector) {}
  
  initializeLogging(): void {
    effect(() => {
      console.log(`Nombre d'utilisateurs : ${this.users().length}`);
    }, {injector: this.injector});
  }
}

Les effets sont automatiquement détruits lorsque leur contexte englobant est détruit. Cela signifie que les effets créés dans les composants sont détruits lorsque le composant est détruit. Il en va de même pour les effets dans les directives, les services, etc.

Attention aux Effets

Les effets doivent être utilisés avec précaution :

  • Ne modifiez pas les signaux à l'intérieur d'un effet pour éviter les boucles infinies
  • Utilisez-les pour des opérations "externes" comme les appels API, les notifications, ou les mises à jour du DOM
  • Pensez à nettoyer les ressources si nécessaire (par exemple, des souscriptions).

Nettoyage des Effets (Cleanup)

Les effets peuvent démarrer des opérations longues qu'il faut annuler si l'effet est détruit ou s'exécute à nouveau avant que la première opération ne soit terminée. Lors de la création d'un effet, votre fonction peut accepter en option un paramètre onCleanup. Cette fonction vous permet d'enregistrer une fonction de rappel qui sera appelée avant la prochaine exécution de l'effet ou lorsque l'effet est détruit.

Voici un exemple concret avec notre application de gestion d'utilisateurs :

typescript
@Component({
  selector: 'app-user-activity',
  template: `
    <div>Statut de l'utilisateur : {{ userStatus() }}</div>
  `
})
export class UserActivityComponent {
  selectedUser = signal<User | null>(null);
  userStatus = signal<string>('En ligne');

  constructor() {
    // Effet qui surveille l'activité de l'utilisateur
    effect((onCleanup) => {
      const user = this.selectedUser();
      if (!user) return;

      // Démarrer un minuteur pour vérifier l'activité
      const timer = setInterval(() => {
        const lastActive = new Date(user.lastActive);
        const fiveMinutesAgo = new Date(Date.now() - 300000);
        
        if (lastActive < fiveMinutesAgo) {
          this.userStatus.set('Inactif');
        }
      }, 60000); // Vérifier toutes les minutes

      // Nettoyer le timer quand :
      // - L'utilisateur sélectionné change
      // - Le composant est détruit
      onCleanup(() => {
        clearInterval(timer);
      });
    });
  }
}

Dans cet exemple :

  1. Nous créons un effet qui surveille l'activité d'un utilisateur
  2. L'effet démarre un setInterval qui vérifie régulièrement si l'utilisateur est actif
  3. La fonction onCleanup est utilisée pour arrêter le timer quand :
    • Un nouvel utilisateur est sélectionné (l'effet s'exécute à nouveau)
    • Le composant est détruit (l'effet est détruit)

Cas d'utilisation du Cleanup

Le nettoyage est particulièrement utile pour :

  • Annuler des requêtes HTTP en cours
  • Nettoyer des timers (setTimeout, setInterval)
  • Se désabonner des observables RxJS
  • Supprimer des écouteurs d'événements
  • Libérer des ressources système

Important

Sans fonction de nettoyage, vous risquez :

  • Des fuites de mémoire
  • Des comportements inattendus
  • Des effets qui continuent de s'exécuter même après la destruction du composant