Appearance
Streaming Resources dans Angular : Gestion élégante des données en temps réel
Version
Cette fonctionnalité est disponible depuis Angular 19.2
Imaginez que vous développez une application de chat en temps réel. Traditionnellement, vous auriez utilisé des WebSockets avec RxJS pour gérer les messages entrants. Mais avec l'introduction des Streaming Resources dans Angular 19.2, nous avons maintenant une approche plus élégante et plus "Angular-native" pour gérer ces flux de données.
Comprendre les Streaming Resources
Les Streaming Resources sont une nouvelle façon de gérer les flux de données en temps réel dans Angular. Contrairement aux Observables RxJS qui représentent un flux continu d'événements individuels, les Streaming Resources représentent un état qui évolue dans le temps.
Différence clé
Alors qu'un Observable WebSocket traditionnel émet chaque message individuellement, un Streaming Resource maintient un état complet qui inclut tous les messages reçus. C'est particulièrement utile pour les applications qui ont besoin de conserver un historique des données reçues.
Implémentation d'un chat avec Streaming Resources
Voyons comment implémenter un chat simple en utilisant les Streaming Resources :
ts
import { resource, signal, computed, Injectable } from '@angular/core';
// Types simplifiés
interface Message {
id: number;
user: string;
text: string;
timestamp: number;
}
@Injectable({
providedIn: 'root',
})
export class ChatService {
// État local
private userName = signal<string>('');
private connection = signal<WebSocket | null>(null);
// État de connexion
readonly isConnected = computed(() => !!this.connection());
// Resource de chat
readonly messages = resource({
request: () => this.userName(),
stream: async ({ request: userName, abortSignal }) => {
const messages: Message[] = [];
const resultSignal = signal({
value: messages,
});
if (!userName) return resultSignal;
// Création de la connexion WebSocket
const ws = new WebSocket('ws://chat-server.com');
this.connection.set(ws);
// Gestion des messages
ws.addEventListener('message', (event: any) => {
const message = JSON.parse(event.data) as Message;
messages.push(message);
resultSignal.set({
value: [...messages],
});
});
// Nettoyage à l'arrêt
abortSignal.addEventListener('abort', () => {
ws.close();
this.connection.set(null);
});
return resultSignal;
},
});
// Méthodes publiques
connect(userName: string) {
this.userName.set(userName);
}
sendMessage(text: string) {
const ws = this.connection();
if (!ws) return;
const message = {
text,
user: this.userName(),
timestamp: Date.now(),
};
ws.send(JSON.stringify(message));
}
}
ts
import { Component, inject } from '@angular/core';
import { ChatService } from './chat.service';
@Component({
selector: 'app-chat',
standalone: true,
template: `
@if (!isConnected()) {
<h1>Connexion</h1>
<div class="login">
<input #nameInput type="text" placeholder="Votre nom">
<button (click)="connect(nameInput.value)">
Se connecter
</button>
</div>
}
@if (isConnected()) {
<h1>Messages</h1>
<div class="chat">
<!-- Messages -->
<div class="messages">
@if (messages.isLoading()) {
<p>Chargement...</p>
}
@for (message of messages.value() ?? []; track message.id) {
<div class="message">
<strong>{{ message.user }}</strong>
<p>{{ message.text }}</p>
</div>
}
</div>
<!-- Formulaire d'envoi -->
<div class="send-form">
<input #msgInput type="text" placeholder="Votre message">
<button (click)="sendMessage(msgInput.value); msgInput.value = ''">
Envoyer
</button>
</div>
</div>
}
`,
})
export class ChatComponent {
private chatService = inject(ChatService);
messages = this.chatService.messages;
isConnected = this.chatService.isConnected;
connect(userName: string) {
this.chatService.connect(userName);
}
sendMessage(text: string) {
if (!text.trim()) return;
this.chatService.sendMessage(text);
}
}
La fonction stream
dans le Resource
La fonction stream
est le cœur du Streaming Resource. Elle reçoit deux paramètres importants :
request
: La valeur actuelle de la requête (dans notre cas, le nom d'utilisateur)abortSignal
: Un signal permettant de gérer le nettoyage des ressources
Structure et type de retour
ts
stream: async ({
request: userName, // La valeur retournée par la fonction request
abortSignal // Signal pour le nettoyage
}): PromiseLike<Signal<{ value: T; } | { error: unknown; }>> => {
// ... code ...
return resultSignal; // Doit retourner un signal avec le bon type
}
Le resultSignal
est un élément crucial que stream
doit retourner. Il doit respecter strictement le type suivant :
- Être une Promise (ou PromiseLike) qui résout vers
- Un Signal qui contient soit :
- Un objet avec une propriété
value
contenant les données de typeT
- OU un objet avec une propriété
error
contenant l'erreur
- Un objet avec une propriété
Exemple de création du signal :
ts
// Pour un succès
const resultSignal = signal({ value: messages });
// Pour une erreur
const resultSignal = signal({ error: new Error('Erreur de connexion') });
Important
- Le signal DOIT toujours contenir soit
value
, soiterror
, jamais les deux - Ne jamais retourner un signal sans ces propriétés
- En cas d'erreur, toujours utiliser la propriété
error
plutôt que de lancer une exception
La gestion du nettoyage avec abortSignal
L'abortSignal
est un mécanisme crucial fourni par Angular pour gérer proprement le nettoyage des ressources dans les Streaming Resources. Il s'agit d'une instance de AbortSignal
, une API web standard.
ts
stream: async ({ abortSignal }) => {
// 1. Création des ressources
const ws = new WebSocket('ws://chat-server.com');
// 2. Configuration du nettoyage
abortSignal.addEventListener('abort', () => {
// Fermeture de la connexion WebSocket
ws.close();
// Réinitialisation de l'état
this.connection.set(null);
});
// ... reste du code ...
}
Imaginez que vous quittez une salle de chat. Vous devez :
- Fermer la connexion WebSocket
- Nettoyer les données en mémoire
- Réinitialiser l'état de connexion
C'est exactement ce que fait l'abortSignal
automatiquement !
Quand l'AbortSignal est-il déclenché ?
L'événement 'abort' est émis automatiquement par Angular dans plusieurs cas :
- Quand le composant est détruit
- Quand la fonction
request()
renvoie une nouvelle valeur - Quand le Resource est explicitement arrêté
Bonnes pratiques
- Toujours utiliser l'
abortSignal
pour nettoyer les ressources externes (WebSocket, EventSource, etc.) - Ne pas oublier de réinitialiser les états internes
- Éviter les fuites mémoire en nettoyant les listeners et les timers
L'objet messages
dans le composant
Dans le composant, messages
est un Streaming Resource qui expose trois propriétés principales :
value()
: Un signal contenant les messages actuelsisLoading()
: Un signal indiquant si le stream est en cours de chargementerror()
: Un signal contenant l'erreur éventuelle
Bonnes pratiques
- Toujours vérifier
messages.value()
avec l'opérateur??
pour gérer le cas où les données sontundefined
- Utiliser
messages.isLoading()
pour afficher un état de chargement - Gérer les erreurs avec
messages.error()
pour une meilleure expérience utilisateur
Avantages des Streaming Resources
Les Streaming Resources offrent plusieurs avantages par rapport à l'approche traditionnelle avec RxJS :
Gestion d'état intégrée : Les Streaming Resources maintiennent naturellement l'état des données reçues, ce qui est parfait pour les cas d'utilisation comme un chat où vous voulez conserver l'historique des messages.
Intégration native avec les Signals : Pas besoin de convertir des Observables en Signals, tout fonctionne nativement avec le système de réactivité d'Angular.
Nettoyage automatique : Le système gère automatiquement la fermeture des connexions et le nettoyage des ressources via l'
abortSignal
.
Bonne pratique
Utilisez les Streaming Resources quand :
- Vous avez besoin de maintenir un état qui évolue dans le temps
- Vous voulez une intégration native avec les Signals d'Angular
- Vous gérez des connexions en temps réel comme WebSocket