Fermer

août 28, 2025

Construire un compte à rebours réactif

Construire un compte à rebours réactif


Un tutoriel complet pour les développeurs angulaires expérimentés pour apprendre les signaux angulaires en créant une application pratiques de temporisation avec des signaux et des effets calculés.

Signaux angulaires représentent un changement fondamental dans la façon dont nous gérons l’état réactif dans les applications angulaires. Si vous venez des observables RXJS ou d’autres bibliothèques de gestion de l’État, les signaux offrent une approche plus intuitive, performante et granulaire de la réactivité. Contrairement à la détection des changements basée sur la zone qui s’exécute pour des arbres de composants entiers, les signaux fournissent une réactivité à grain fin qui ne met à jour que ce qui a réellement changé.

Dans ce didacticiel, nous allons créer une application de compte à rebours pratique qui démontre les concepts de base des signaux angulaires. Vous apprendrez comment les signaux fournissent un suivi automatique des dépendances, éliminez le besoin d’abonnements manuels et créent des applications réactives plus prévisibles.

Exemple de vue d’ensemble: temporisation réactive du compte à rebours

Un temporisateur à rebours est un excellent exemple pour l’apprentissage des signaux car il implique plusieurs états réactifs qui dépendent les uns des autres. La minuterie montrera comment les signaux gèrent naturellement:

  • Gestion de l’État: Durée de la minuterie, état actuel et fonctionnement
  • État dérivé: Affichage du temps formaté et pourcentage de progression
  • Effets secondaires: Alertes d’achèvement et mises à jour d’interface utilisateur

Les applications du monde réel pour ce modèle comprennent:

  • Studios d’enregistrement: Session Time Tracking and Break Timers
  • Événements en direct: Limites de temps et comptabilité de présentation du conférencier
  • Applications de productivité: Pomodoro Minères et séances de concentration
  • Applications de fitness: Intervalles d’entraînement et périodes de repos

Il comportera des commandes de démarrage / stop / réinitialisation, des boutons de temps prédéfinis, des indicateurs de progression visuelle et des messages d’achèvement. Tous construits avec des signaux pour présenter leurs capacités réactives.

Configuration du projet

Commençons par créer un nouveau projet angulaire avec la dernière version qui inclut le support des signaux:

npm install -g @angular/cli
ng new countdown-timer --routing=false --style=css
cd countdown-timer

Vous pouvez utiliser NPX si vous ne souhaitez pas installer Angular CLI globalement: npx -p @angular/cli@20 ng new countdown-timer --routing=false --style=css.

Créez le composant de la minuterie:

ng generate component countdown-timer --standalone

Mise à jour src/app/app.ts Pour importer et utiliser le composant de temporisation à rebours:

import { CountdownTimer } from "./countdown-timer/countdown-timer";

@Component({
  
  imports: [CountdownTimer],
})

Mise à jour src/app/app.html Pour rendre le nouveau composant:

<div class="app-container">
  <h1>Angular Signals Countdown Timer</h1>
  <app-countdown-timer></app-countdown-timer>
</div>

Ajouter un style de base à src/app/app.css:

.app-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}

h1 {
  color: #2c3e50;
  margin-bottom: 2rem;
}

Application angulaire de base avec le comptoir du comptoir d'espace réservé

Fondamentaux des signaux angulaires

Les signaux sont des primitives réactives qui détiennent des valeurs et informent les consommateurs lorsque ces valeurs changent. Commençons par mettre en œuvre l’état de la minuterie de base dans le CountdownTimer composant. Copiez et collez le code ci-dessous dans src/app/countdown-timer/countdown-timer.ts déposer:

import { Component, signal, computed, effect, OnDestroy } from "@angular/core";
import { CommonModule } from "@angular/common";

@Component({
  selector: "app-countdown-timer",
  standalone: true,
  imports: [CommonModule],
  templateUrl: "./countdown-timer.html",
  styleUrl: "./countdown-timer.css",
})
export class CountdownTimer implements OnDestroy {
  
  private timeRemaining = signal(60); 
  private isRunning = signal(false);
  private initialTime = signal(60);

  
  readonly timeLeft = this.timeRemaining.asReadonly();
  readonly running = this.isRunning.asReadonly();

  private intervalId: number | null = null;

  constructor() {
    console.log("Timer initialized with:", this.timeLeft());
  }

  ngOnDestroy(): void {
    
    this.stop();
  }
}

Le code crée un signal écrivative avec une valeur initiale en utilisant la syntaxe signal(initialValue). Les signaux écrit fournissent une API pour mettre à jour directement leurs valeurs. Par la suite, il crée des versions en lecture seule de ces signaux à utiliser dans le modèle.

Il garde une référence au intervalId Pour vous assurer que la minuterie est nettoyée correctement. Vous avez peut-être remarqué que, contrairement aux observables, les signaux ne nécessitent pas d’abonnements explicites. Il garde une trace de l’endroit et de la façon dont ils sont utilisés et les mette automatiquement.

Construire la logique de la minuterie de base

Implémentez maintenant la fonctionnalité du temporisateur avec la gestion de l’état basée sur le signal. Ajouter le code suivant au CountdownTimer composant:

export class CountdownTimer {
  

  
  start(): void {
    if (this.isRunning()) return;

    this.isRunning.set(true);

    this.intervalId = window.setInterval(() => {
      this.timeRemaining.update((time) => {
        if (time <= 1) {
          this.stop();
          return 0;
        }
        return time - 1;
      });
    }, 1000);
  }

  stop(): void {
    this.isRunning.set(false);
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  reset(): void {
    this.stop();
    this.timeRemaining.set(this.initialTime());
  }

  setTime(seconds: number): void {
    this.stop();
    this.initialTime.set(seconds);
    this.timeRemaining.set(seconds);
  }
}

Le set() et update() Les méthodes modifient la valeur d’un signal écrit. La différence entre eux est que le .update() La méthode reçoit la valeur actuelle et renvoie la nouvelle valeur. Cette approche fonctionnelle permet l’immuabilité et rend les changements d’État prévisibles.

Signaux calculés

Les signaux calculés tirent leurs valeurs à partir d’autres signaux et recalculent automatiquement lorsque les dépendances changent. Ajoutez ces états dérivés au composant:

export class CountdownTimer implements OnDestroy {
  

  
  readonly formattedTime = computed(() => {
    const time = this.timeLeft();
    const minutes = Math.floor(time / 60);
    const seconds = time % 60;
    return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
  });

  readonly progressPercentage = computed(() => {
    const initial = this.initialTime();
    const remaining = this.timeLeft();
    if (initial === 0) return 0;
    return ((initial - remaining) / initial) * 100;
  });

  readonly isCompleted = computed(() => this.timeLeft() === 0);

  readonly buttonText = computed(() => (this.running() ? "Pause" : "Start"));
}

Les signaux calculés sont en lecture seule et évalué paresseusement– Ils ne recalculent que lorsqu’ils sont accessibles et lorsque leurs dépendances changent. Ceci est plus efficace que la gestion manuelle de l’état dérivé avec des observables.

Effets du signal

Les effets sont des opérations asynchrones qui s’exécutent lorsqu’un ou plusieurs signaux changent. Ils sont utiles pour des tâches telles que la journalisation, l’analyse ou l’ajout d’un comportement DOM personnalisé qui ne peut pas être exprimé avec la syntaxe du modèle. Ajoutez les effets suivants pour enregistrer les informations à la fin du compte à rebours:

export class CountdownTimer implements OnDestroy {
  

  constructor() {
    
    effect(() => {
      console.log(
        `Timer state: ${this.formattedTime()}, Running: ${this.running()}`,
      );
    });

    
    effect(() => {
      if (this.isCompleted()) {
        
        this.onTimerComplete();
      }
    });
  }
  
  private onTimerComplete(): void {
    
    
    console.log("Timer has completed - handle completion here");
  }

  ngOnDestroy(): void {
    
    this.stop();
  }
}

Les effets suivent automatiquement leurs dépendances de signal et relancent lorsqu’une dépendance change. Contrairement aux abonnements RXJS, vous n’avez pas besoin de vous désinscrire manuellement car Angular gère automatiquement le nettoyage lorsque le composant est détruit.

Important: Évitez d’utiliser des effets pour la propagation des changements d’état. Utilisez plutôt des signaux calculés. Les effets ne doivent être utilisés que pour les effets secondaires comme la manipulation DOM qui ne peut pas être exprimé avec la syntaxe du modèle.

Visualiser la minuterie

Il est temps de vous montrer comment utiliser les signaux pour construire des interfaces utilisateur réactives. Ajoutons les boutons de temps prédéfinis et un indicateur de progression visuelle. Mettez à jour votre modèle (countdown-timer.html):

<div class="timer-container">
  
  <div class="time-display">{{ formattedTime() }}</div>

  
  <div class="progress-container">
    <div class="progress-bar" [style.width.%]="progressPercentage()"></div>
  </div>

  
  <div class="controls">
    <button (click)="running() ? stop() : start()" [disabled]="isCompleted()">
      {{ buttonText() }}
    </button>

    <button (click)="reset()">Reset</button>
  </div>

  
  <div class="presets">
    <button (click)="setTime(30)" [disabled]="running()">30s</button>
    <button (click)="setTime(60)" [disabled]="running()">1min</button>
    <button (click)="setTime(300)" [disabled]="running()">5min</button>
    <button (click)="setTime(600)" [disabled]="running()">10min</button>
  </div>
</div>

Ajoutez les styles correspondants (countdown-timer.css):

.timer-container {
  background: #f8f9fa;
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.time-display {
  font-size: 4rem;
  font-weight: bold;
  color: #2c3e50;
  margin-bottom: 1.5rem;
  font-family: "Courier New", monospace;
}

.progress-container {
  width: 100%;
  height: 8px;
  background-color: #e9ecef;
  border-radius: 4px;
  margin-bottom: 2rem;
  overflow: hidden;
}

.progress-bar {
  height: 100%;
  background: linear-gradient(90deg, #28a745, #ffc107, #dc3545);
  transition: width 0.3s ease;
}

.controls,
.presets {
  display: flex;
  gap: 1rem;
  justify-content: center;
  margin-bottom: 1rem;
}

button {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 6px;
  background: #007bff;
  color: white;
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.2s;
}

button:hover:not(:disabled) {
  background: #0056b3;
}

button:disabled {
  background: #6c757d;
  cursor: not-allowed;
}

Remarquez comment le modèle utilise directement les valeurs du signal avec la syntaxe de l’appel de fonction: formattedTime(), progressPercentage(), running(). Le moteur de modèle d’Angular suit automatiquement ces dépendances et met à jour uniquement les nœuds DOM affectés.

Signal vs observables RXJS

Les signaux offrent plusieurs avantages par rapport à l’ancienne approche observable:


export class TraditionalComponent {
  private timeSubject = new BehaviorSubject(60);
  time$ = this.timeSubject.asObservable();

  formattedTime$ = this.time$.pipe(
    map((time) => {
      const minutes = Math.floor(time / 60);
      const seconds = time % 60;
      return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
    }),
  );

  ngOnDestroy() {
    
    this.timeSubject.complete();
  }
}


export class SignalComponent {
  private timeRemaining = signal(60);

  readonly formattedTime = computed(() => {
    const time = this.timeRemaining();
    const minutes = Math.floor(time / 60);
    const seconds = time % 60;
    return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
  });

  
}

Avantages clés:

  • Mises à jour granulaires: Seuls les composants utilisant des signaux modifiés rerender
  • Aucune gestion d’abonnement: Suivi et nettoyage des dépendances automatique
  • Pas de complexité de tuyau asynchrone dans les modèles
  • Arbre-taignable: Les calculs inutilisés sont automatiquement optimisés

Meilleures pratiques

1. Utilisez des signaux Readonly pour les API publiques

private _count = signal(0);
readonly count = this._count.asReadonly();

2. Préférez les signaux calculés à la dérivation manuelle


readonly isEven = computed(() => this.count() % 2 === 0);


get isEven() { return this.count() % 2 === 0; }

3. Utiliser des effets avec parcimonie pour les effets secondaires uniquement


effect(() => {
  localStorage.setItem("timerState", JSON.stringify(this.timerState()));
});

effect(() => {
  console.log(`Timer state changed: ${this.formattedTime()}`);
});


effect(() => {
  if (this.isCompleted()) {
    alert("Done!"); 
  }
});

4. Implémentez un nettoyage approprié pour les composants avec des intervalles / minuteries

export class TimerComponent implements OnDestroy {
  ngOnDestroy(): void {
    this.stop(); 
  }
}

5. Gardez les mises à jour du signal simple et prévisible


this.count.update((n) => n + 1);


this.count.update((n) => {
  
  return someComplexCalculation(n);
});

Conclusion

Les signaux angulaires représentent un changement de paradigme vers une programmation réactive plus intuitive et plus performante. Grâce à la construction de cette minuterie à rebours, vous avez appris ses concepts principaux et ses meilleures pratiques.

La minuterie montre comment les signaux gèrent naturellement des scénarios réactifs complexes avec des signaux écrivains, des signaux et des effets calculés. La nature déclarative des signaux calculés et le suivi automatique des dépendances rendent votre code plus prévisible et plus facile à raisonner.

Suivez ces étapes pour adopter des signaux dans vos applications de production:

  1. Commencer petit: Convertir l’état réactif simple des observables en signaux
  2. Identifier les valeurs calculées: Recherchez un état dérivé qui peut devenir des signaux calculés
  3. Migrer progressivement: Les signaux s’interoppent bien avec les observables pendant la transition
  4. Les effets des effets: Remplacer les effets secondaires basés sur l’abonnement par des effets de signal

Alors qu’Angular continue d’évoluer, les signaux deviendront de plus en plus centraux du modèle réactif du cadre.

Ressources supplémentaires




Source link