Skip to content

Stocker un état dans un service avec BehaviorSubject

HttpClientModule

Pour utiliser le service HTTP dans Angular, vous pouvez d'abord inclure HttpClientModule dans votre application en l'ajoutant à la liste des imports dans votre fichier de module principal (par exemple, app.module.ts).

ts
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  // ... other imports
  imports: [
    // ... other imports
    HttpClientModule,
  ],
})
export class AppModule { }
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  // ... other imports
  imports: [
    // ... other imports
    HttpClientModule,
  ],
})
export class AppModule { }

L'interface User en TypeScript de la requête

Voici un exemple d'interface qui pourrait être utilisée pour représenter les utilisateurs de cette URL:

ts
// user.interface.ts
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    }
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}
// user.interface.ts
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    }
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

Cette interface définit les propriétés des utilisateurs qui peuvent être retournés par l'URL https://jsonplaceholder.typicode.com/users, telles que l'identifiant de l'utilisateur, son nom, son nom d'utilisateur, son adresse électronique, etc. Vous pouvez utiliser cette interface pour stocker et manipuler les données des utilisateurs de cette URL dans votre application TypeScript.

Service UserService

Voici comment votre service pourrait être implémenté si la méthode setUsers faisait une requête HTTP pour récupérer un nouveau tableau d'utilisateurs à partir de l'URL https://jsonplaceholder.typicode.com :

ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, catchError, map } from 'rxjs';
import { HttpClient } from '@angular/common/http';

import { User } from '../models/user';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private usersSubject = new BehaviorSubject<User[]>([]);
  public users = this.usersSubject.asObservable();

  constructor(private http: HttpClient) { }

  public setUsers() {
    this.http.get<User[]>('https://jsonplaceholder.typicode.com/users')
      .pipe(
        catchError(() => {
          this.usersSubject.error('An error occurred');
          return [];
        }),
        map((users) => {
          // traitement des données avant de mettre à jour l'état courant
          return users;
        })
      )
      .subscribe((users) => {
        this.usersSubject.next(users);
      });
  }
}
import { Injectable } from '@angular/core';
import { BehaviorSubject, catchError, map } from 'rxjs';
import { HttpClient } from '@angular/common/http';

import { User } from '../models/user';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private usersSubject = new BehaviorSubject<User[]>([]);
  public users = this.usersSubject.asObservable();

  constructor(private http: HttpClient) { }

  public setUsers() {
    this.http.get<User[]>('https://jsonplaceholder.typicode.com/users')
      .pipe(
        catchError(() => {
          this.usersSubject.error('An error occurred');
          return [];
        }),
        map((users) => {
          // traitement des données avant de mettre à jour l'état courant
          return users;
        })
      )
      .subscribe((users) => {
        this.usersSubject.next(users);
      });
  }
}

Dans ce service, vous utilisez la méthode get de l'objet HttpClient pour faire une requête HTTP et récupérer un tableau d'utilisateurs. Vous utilisez ensuite les opérateurs catchError et map pour traiter les erreurs et les données reçues avant de mettre à jour l'état courant en appelant la méthode next de votre instance de BehaviorSubject.

Pourquoi utiliser BehaviorSubject au lieu de Subject ?

Le BehaviorSubject est similaire au Subject, mais il a une valeur courante qui est initialisée lors de sa création et qui est retournée aux abonnés qui s'abonnent après que la valeur courante a été publiée. Cela signifie que le BehaviorSubject permet de stocker l'état courant d'une valeur et de le retourner aux abonnés qui s'abonnent après qu'une valeur a été publiée.

Voici un exemple de code qui illustre cela :

ts
import { BehaviorSubject } from 'rxjs';

const subject = new BehaviorSubject<string>('Initial value');

subject.subscribe((value) => {
  console.log(value); // affiche 'Initial value'
});

subject.next('A value');

subject.subscribe((value) => {
  console.log(value); // affiche 'A value'
});
import { BehaviorSubject } from 'rxjs';

const subject = new BehaviorSubject<string>('Initial value');

subject.subscribe((value) => {
  console.log(value); // affiche 'Initial value'
});

subject.next('A value');

subject.subscribe((value) => {
  console.log(value); // affiche 'A value'
});

Le BehaviorSubject est particulièrement utile dans les cas où vous souhaitez stocker et partager l'état courant d'une valeur avec plusieurs composants. Par exemple, vous pouvez utiliser un BehaviorSubject dans un service pour stocker et partager l'état courant d'un tableau d'utilisateurs avec plusieurs composants qui en ont besoin.

Pourquoi utiliser asObservable ?

La méthode asObservable permet de retourner un observable à partir d'un Subject, mais en empêchant les abonnés d'appeler la méthode next sur l'observable retourné. Cela signifie que seul le Subject original peut être utilisé pour publier de nouvelles valeurs et pour notifier les abonnés des mises à jour.

Voici un exemple de code qui illustre cela :

ts
import { Subject } from 'rxjs';

const subject = new Subject<string>();
const observable = subject.asObservable();

subject.next('A value'); // la valeur est publiée et les abonnés sont notifiés
observable.next('A value'); // génère une erreur car la méthode next est privée

observable.subscribe((value) => {
  console.log(value); // affiche 'A value'
});
import { Subject } from 'rxjs';

const subject = new Subject<string>();
const observable = subject.asObservable();

subject.next('A value'); // la valeur est publiée et les abonnés sont notifiés
observable.next('A value'); // génère une erreur car la méthode next est privée

observable.subscribe((value) => {
  console.log(value); // affiche 'A value'
});

Lire les données dans le composant (pipe async)

Voici comment votre composant pourrait être implémenté si vous utilisiez le pipe async pour vous abonner aux données du service :

ts
import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
import { Observable } from 'rxjs';

import { User } from '../models/user';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
  styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
  users$: Observable<User[]> = this.userService.users

  constructor(private userService: UserService) { }

  ngOnInit() {
    this.userService.setUsers();
  }
}
import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
import { Observable } from 'rxjs';

import { User } from '../models/user';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
  styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
  users$: Observable<User[]> = this.userService.users

  constructor(private userService: UserService) { }

  ngOnInit() {
    this.userService.setUsers();
  }
}

Dans votre template, vous pouvez utiliser le pipe async pour afficher les données de manière asynchrone :

html
<ng-container *ngIf="(users$ | async) as users">
  <ul>
    <li *ngFor="let user of users">{{ user.name }}</li>
  </ul>
</ng-container>
<ng-container *ngIf="(users$ | async) as users">
  <ul>
    <li *ngFor="let user of users">{{ user.name }}</li>
  </ul>
</ng-container>

Le pipe async vous permet de vous abonner aux données de manière asynchrone et de les utiliser dans votre template de manière transparente. Vous n'avez pas à vous abonner manuellement aux données dans votre composant ni à utiliser des indicateurs de chargement ou des messages d'erreur dans votre template.

Gérer les erreurs dans template

Pensez à la manière dont vous souhaitez gérer les erreurs et les cas où les données ne sont pas disponibles. Vous pouvez par exemple utiliser la directive ngIf avec une expression de contrôle d'erreur pour afficher un message d'erreur lorsque nécessaire :

html
<ng-container *ngIf="(users$ | async) as users; else error">
  <ul>
    <li *ngFor="let user of users">{{ user.name }}</li>
  </ul>
</ng-container>
<ng-template #error>An error occurred</ng-template>
<ng-container *ngIf="(users$ | async) as users; else error">
  <ul>
    <li *ngFor="let user of users">{{ user.name }}</li>
  </ul>
</ng-container>
<ng-template #error>An error occurred</ng-template>

Vous pouvez également utiliser la directive ngIf avec une expression de vérification de valeur nulle pour afficher un message indiquant que les données sont en cours de chargement ou que les données sont vides :

html
<ng-container *ngIf="users$ | async as users; else loading">
  <ng-container *ngIf="users.length; else noData">
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
  </ng-container>
  <ng-template #noData>No data</ng-template>
</ng-container>
<ng-container *ngIf="users$ | async as users; else loading">
  <ng-container *ngIf="users.length; else noData">
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
  </ng-container>
  <ng-template #noData>No data</ng-template>
</ng-container>