Le logo de la librairie RXJS

L'introduction

Dans une application web, l’asynchrone est présent de partout, c’est ce qui permet d’apporter de la réactivité, mais cette asynchrone fonctionne de bien des façons différentes. Nous avons les promesses d’un côté et les Observables de l’autre. C’est sur cette partie que nous allions nous attarder aujourd’hui et plus particulièrement les **Subjects** et **BehaviorSubjects.**

Les Subjects

Le subject est un type spécial d’Observable, il partage un chemin d’exécution unique entre tous les observateurs qui se sont abonnée aux modifications. Dans un subject, aucune valeur n’est définie par défaut et les abonnés ne recevront que les valeurs qui seront émises après une modification de la valeur actuelle.

Les BehaviorSubjects

Le BehaviorSubject a un comportement presque identique à un subject, sauf qu’il a besoin d’une valeur initiale lors de sa création et il transmet automatiquement la dernière valeur aux nouveaux abonnés.


La meilleure façon d’appréhender ces deux types d’observables, c’est de se pencher sur des cas pratiques. Nous allons donc voir des cas pratiques d’utilisation de ces 2 types d’observables.

Un store pour nos données

Nous allons voir la façon la plus simple de partager ses données au sein d’une application angular, en créant un store réactif basé sur les Observables.

@Injectable({
  providedIn: 'root'
})
export class DataStore {

  private $Data: BehaviorSubject<any>=new BehaviorSubject<any>([]);

  public get $data(): Observable<any> {
    return this.$Data.asObservable();
  }

  public storeGlobalData(value: any): void {
    this.$Data.next(value);
  }

}

Dans notre store nous créons donc un behaviorsubject qui va contenir notre donnée, un getter et une méthode pour alimenter notre Observable.

@Injectable({
  providedIn: 'root'
})
export class DataService {

  constructor(private readonly httpService: HttpService,
              private readonly store: DataStore) {
  }

  public getAllData(pageNumber: number) {

    return this.httpService.getAllData(pageNumber)
      .pipe(
        tap((data) => this.store.storeGlobalData(data)),
        catchError((err) => handleError(err))
      )
  }

  public getOnData(data: any) {
    return this.httpService.getOneData(data)
      .pipe(
        tap((data)=>this.store.storeDetailData(data)),
        catchError((err) => handleError(err))
      )
  }

  public searchData(params: FilterConfiguration[]) {
    return this.httpService.searchData(params)
      .pipe(
        tap((data) => this.store.storeGlobalData(data)),
        catchError((err) => handleError(err))
      )
  }

}

Dans notre DataService, nous allons utiliser la méthode exposée dans notre store pour venir ajouter les données que l’on reçoit du back-end.

@Injectable({
  providedIn: 'root'
})
export class DataResolver implements Resolve<unknown> {

  constructor(private readonly dataService: DataService) {
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<unknown> {
    return this.dataService.getAllData(0);
  }

}

Nous mettons en place un resolver afin d’utiliser notre service au moment où on en a besoin au niveau du routing.

{
  path: "route/de/la/data",
  component: DataComponent,
  resolve: { data: DataResolver }
}

Nous ajoutons notre resolver dans le routing de notre application, ça va permettre à la route d’exécuter le service au moment où nous allons arriver dessus.

ngOnInit(): void {

  this.sub.add(
    this.store.$data
      .pipe(
        tap((data) => this.items = data)
      )
      .subscribe()
  );

}

Un state manager pour notre pagination front

L’idée ici, c’est de centraliser la gestion de nos paginations de tableau dans un composant abstrait et que notre composant tableau en hérite pour l’utiliser.

@Component({
  selector: 'base-table-view',
  template: '',
  styleUrls: []
})
export abstract class BaseTableViewComponent {

  protected currentPageNavigation: Subject<number> = new Subject<number>();

  protected constructor(protected readonly router: Router) {
  }

  protected setCurrentState(value: number): void {
    this.currentPageNavigation.next(value)
  }

  protected resetNavigation(){
    this.currentPageNavigation.next();
    this.currentPageNavigation.complete();
  }

}

Nous avons donc ici un composant abstrait, qui ne possède pas de template, pas de CSS, juste un subject et des méthodes de gestion pour mettre à jour le subject et reset la navigation.

@Component({
  selector: "data-table-view",
  templateUrl: "./legacy-patient.component.html",
  styleUrls: ["./legacy-patient.component.scss"]
})
export class DataTableViewComponent extends BaseTableViewComponent implements OnInit, OnDestroy {
...
public getCurrentNavigationState(evt: PageEvent) {
  this.setCurrentState(evt.pageIndex);
}
ngOnInit(): void {
  this.sub.add(
    this.currentPageNavigation
      .pipe(
        switchMap((r:number)=>this.dataService.getAllData(result))
      )
      .subscribe()
  );
}
ngOnDestroy(){
  resetNavigation()
}
...
}

Nous voyons ici l’utilisation des méthodes du composant abstrait pour gérer la pagination dans le composant data-table. Dans le ngOnInit, on va s’abonner aux modifications et lancer la requête de récupération des données à chaque modification. Dans le ngOnDestroy, on reset la navigation pour être tranquille et éviter les effets de bord. La méthode getcurrentnavigationstate va permettre de mettre à jour le subject et récupérer l’état de la pagination


Nous avons pu voir ici deux cas pratiques qui permettent de mieux appréhender la notion de subject et debehaviorsubject dans les applications angular. Il en existe encore d'autres mais je vous laisse le plaisir d'expérimenter et d'essayer afin de mieux appréhender ça.