Skyler De Francesca
Observables
L’Observable est le type d’objet principal de RxJS. Nous pouvons l’« écouter », ou l’« observer », pour réagir à ses émissions. Pour écouter ces émissions, il faut appeler la fonction subscribe() de l’Observable à l’endroit où vous pouvez accéder à la valeur émise. Il est impossible d’invoquer l’événement ou le changement de valeur à l’aide d’un simple Observable. Il faut le comprendre comme un objet en « lecture seule ». Pour cette raison, il est généralement conseillé d’exposer les Observables lorsque vous ne souhaitez pas que d’autres composants de l’application invoquent des événements, c’est-à-dire, quand vous voulez qu’ils écoutent seulement.
Exemple : le client HTTP utilise un Observable pour exposer l’événement à une requête HTTP.
/**
* get function returns an Observable that will emit an event
* when the response is received
*/
this.httpClient.get<Inventory[]>('https://sample-api/items').subscribe(items => {
// Do something with response
});
Subjects
Les Subjects sont des types d’Observables. En revanche, contrairement aux Observables, les Subjects peuvent émettre des événements/valeurs aux observateurs inscrits à l’aide de la fonction next(). Ainsi, vous pouvez publier des modifications vers un Subject, avec la fonction next(), et écouter des modifications, avec la fonction subscribe(). Vous pouvez également déguiser un Subject en Observable pour cacher son comportement de Subject et n’exposer que sa capacité à souscrire à des modifications.
Les Subjects et les BehaviorSubjects présentent les différences suivantes :
- Les Subjects n’ont pas de valeur initiale.
- Les observateurs ne seront notifiés et ne recevront les événements/valeurs qu’une fois souscrits : au moment de leur inscription, ils ne recevront pas la dernière valeur qui a été émise.
Par exemple, imaginons qu’on veuille utiliser des Subjects pour notifier les observateurs qu’un événement a été émis :
const subject = new Subject();
subject.next('event 0');
subject.subscribe(event => console.log(event));
subject.next('event 1');
subject.next('event 2');
subject.next('event 3');
/**
* Expected output:
* event 1
* event 2
* event 3
*/
Comme l’événement 0 a été émis avant la souscription, l’observateur ne recevra pas la valeur. Si le programme exige qu’un Subject émette cette valeur initiale, alors il est plus pertinent d’avoir recours à un BehaviorSubject.
BehaviorSubjects
Les BehaviorSubjects sont des types de Subjects aux propriétés suivantes :
- Ils ont une valeur initiale.
- Au moment de leur inscription, les observateurs recevront la dernière valeur qui a été émise.
En utilisant le même code que pour le Subject, mais en remplaçant celui-ci par un BehaviorSubject, nous pourrons constater que l’événement 0 est émis. Comme vous le remarquez, nous devons ajouter une valeur initiale (« event -1 ») à la création du BehaviorSubject.
const behaviorSubject = new BehaviorSubject('event -1');
behaviorSubject.next('event 0');
behaviorSubject.subscribe(event => console.log(event));
behaviorSubject.next('event 1');
behaviorSubject.next('event 2');
behaviorSubject.next('event 3');
/**
* Expected output:
* event 0
* event 1
* event 2
* event 3
*/
Comment choisir entre Observables, Subjects et BehaviorSubjects?
Les Subjects sont parfaits lorsque vous souhaitez émettre un événement dont l’état n’est pas important, c’est-à-dire quand l’observateur n’a pas besoin de recevoir les valeurs antérieures à l’inscription.
Les BehaviorSubjects, à l’opposé, sont indiqués si les observateurs ont besoin d’accéder aux valeurs antérieures à leur inscription (« état » actuel de l’événement).
Les Observables sont utiles pour exposer des Subjects ou des BehaviorSubjects à d’autres composants de l’application tout en cachant la capacité à émettre des valeurs/modifications.
Observables, Subjects et BehaviorSubjects : exemples d’utilisation
Imaginons que nous utilisons un service de connexion pour authentifier un utilisateur et stocker ses données de profil. Le code ci-dessous permet de créer ce simple service en utilisant des Observables, des Subjects et des BehaviorSubjects de manière relativement réaliste.
interface UserProfile {
username: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class LoginService {
constructor(private httpClient: HttpClient) { }
/**
* This subject is used to emit the event of a
* successful (true) or unsuccessful (false)
* login attempt.
*/
private _loginSuccess$ = new Subject<boolean>();
/**
* This BehaviourSubject is used to hold and emit the data of the
* logged in user, or undefined if the user is not logged in
*
*/
private _userProfile$ = new BehaviorSubject<UserProfile | undefined>(undefined);
/**
* Expose _loginSuccess$ as observable to conceal
* subject like behaviour.
*/
public get loginSuccess(): Observable<boolean> {
return this._loginSuccess$;
}
/**
* Expose _userProfile$ as Observable to conceal
* subject like behavior.
*/
public get userProfile(): Observable<UserProfile | undefined> {
return this._userProfile$;
}
public login(username: string) {
// Call fake login api and get response
this.httpClient.post<UserProfile>('https://fake-api/login', { username }).subscribe({
// next will be triggered if http request is successful
next: (userData) => {
// invoke _loginSuccess$ subject to emit true
this._loginSuccess$.next(true);
// invoke _userProfile$ behaviourSubject to emit and store response data
this._userProfile$.next(userData)
},
// if login is unsuccessful, invoke _loginSuccess$ subject to emit false
error: () => this._loginSuccess$.next(false)
});
}
}
Tout d’abord, nous utilisons un Subject pour émettre les événements correspondant à une connexion réussie ou échouée. Dans cet exemple, nous n’avons pas besoin de valeur initiale, c’est pourquoi nous préférons un Subject.
private _loginSuccess$ = new Subject<boolean>();
Dans la fonction de connexion, nous appelons la fonction next() de ce Subject dès qu’une réponse (true) ou qu’une erreur (false) est reçue de l’API de connexion. Ainsi, les observateurs seront notifiés à chaque tentative de connexion (réussie ou échouée).
...subscribe({
next: (userData) => {
this._loginSuccess$.next(true);
},
error: () => this._loginSuccess$.next(false)
});
Ensuite, notre BehaviorSubject retient la valeur correspondant au profil de l’utilisateur connecté. Puisque nous voulons que les futurs observateurs aient accès à la dernière valeur émise, il est pertinent d’utiliser un BehaviorSubject.
private _userProfile$ = new BehaviorSubject<UserProfile | undefined>(undefined);
Nous appelons la fonction next() pour fixer la valeur de ce BehaviorSubject lorsque la requête HTTP de connexion est validée.
...subscribe({
next: (userData) => {
this._userProfile$.next(userData)
}
});
Enfin, nous exposons le Subject _loginSuccess$ et le BehaviorSubject _userProfile$ grâce à des fonctions getter, qui sont des types d’Observables. Ainsi, nous pouvons n’exposer que les capacités de souscription/d’annulation de souscription des Subjects.
public get loginSuccess(): Observable<boolean> {
return this._loginSuccess$;
}
public get userProfile(): Observable<UserProfile | undefined> {
return this._userProfile$;
}
Cet exemple reposant sur un service de connexion devrait vous donner une bonne idée des situations dans lesquelles on utilise les Observables, les Subjects et les BehaviorSubjects. Il peut être utile d’observer la manière dont un client interagit avec ce service pour mieux comprendre les utilisations possibles. Voici un exemple montrant un composant interagissant avec ce service :
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit, OnDestroy {
constructor(private loginService: LoginService) { }
// Store profile observable which we can display after login
profile = this.loginService.userProfile;
// Can be used to display loading animation
loading = false;
// Displays success/error message for login
loginMessage = '';
// Holds array of subscriptions made during component lifetime.
subscriptions: Subscription[] = []
ngOnInit() {
// Add subscription to subscriptions array
this.subscriptions.push(
// listen for login success/error
this.loginService.loginSuccess.subscribe(success => {
this.loading = false;
this.loginMessage = success ? 'Login was successful' : 'Error logging in';
})
);
}
login(username: string) {
this.loading = true;
// Calls login function in service to trigger login
this.loginService.login(username)
}
ngOnDestroy(): void {
// When component is destroyed, it is important to clean up subscriptions
this.subscriptions.forEach(sub => sub.unsubscribe())
}
}
Premièrement, nous retenons une référence à l’Observable du profil utilisateur exposé par notre service.
// Store profile observable which we can display after login
profile = this.loginService.userProfile;
Remarquez que nous n’utilisons cette variable de profil à aucun autre endroit du code ci-dessus. Maintenant, nous pouvons utiliser le pipe async pour afficher le profil dans la partie HTML du composant. Le pipe async va souscrire à l’Observable en arrière-plan, nous évitant d’avoir à souscrire manuellement dans le composant.
<div class="profile">
<div>
{{(profile | async)?.email}}
</div>
<div>
{{(profile | async)?.username}}
</div>
</div>
Ensuite, nous utilisons un tableau « subscriptions » pour retenir les souscriptions aux Observables qui durent plus longtemps que le composant. Ainsi, nous pouvons annuler une souscription à ces Observables quand le composant est détruit avec la fonction ngOnDestroy(). Annuler ces souscriptions est important pour éviter les fuites de mémoire. Remarquez que le pipe async annulera automatiquement la souscription à la destruction du composant, il n’est donc pas nécessaire de nettoyer manuellement le profil.
// Holds array of subscriptions made during component lifetime.
subscriptions: Subscription[] = []
ngOnDestroy(): void {
// When component is destroyed, it is important to clean up subscriptions
this.subscriptions.forEach(sub => sub.unsubscribe())
}
Dans ngOnInit, nous souscrivons à l’Observable de connexion réussie (en réalité, un Subject qui se comporte comme un Observable) et réagissons à tous les événements qu’il émet. Ici, nous pouvons manipuler un booléen utilisé pour une animation de chargement et/ou afficher un message de validation ou d’échec (connexion réussie ou échouée) Nous ajoutons la souscription au tableau subscriptions pour pouvoir l’annuler quand le composant est détruit.
ngOnInit() {
// Add subscription to subscriptions array
this.subscriptions.push(
// listen for login success/error
this.loginService.loginSuccess.subscribe(success => {
this.loading = false;
this.loginMessage = success ? 'Login was successful' : 'Error logging in';
})
);
}
Enfin, nous utilisons une fonction qui déclenche la requête HTTP de connexion dans le service. Elle peut être activée par une action du côté utilisateur, comme un clic sur un bouton « soumettre » dans le formulaire de connexion du composant.
login(username: string) {
this.loading = true;
// Calls login function in service to trigger login
this.loginService.login(username)
}
Ensemble, les classes RxJS LoginService et LoginComponent montrent un exemple pratique et complet de la puissance qu’elles peuvent apporter à votre application.
Conclusion
RxJS est une bibliothèque puissante et diverse très fréquemment utilisée dans le développement d’applications dans Angular. Dans le présent guide, nous n’avons fait qu’effleurer sa surface. Comprendre la nature et le fonctionnement des Observables, des Subjects et des BehaviorSubjects est un bon début avant de maîtriser RxJS et Angular.