web-dev-qa-db-fra.com

RxJS: takeUntil () Angular du composant)

tl; dr: je veux fondamentalement épouser le ngOnDestroy de Angular avec l'opérateur Rxjs takeUntil(). - est-ce possible?

J'ai un composant Angular qui ouvre plusieurs abonnements Rxjs. Ceux-ci doivent être fermés lorsque le composant est détruit.

Une solution simple pour cela serait:

class myComponent {

  private subscriptionA;
  private subscriptionB;
  private subscriptionC;

  constructor(
    private serviceA: ServiceA,
    private serviceB: ServiceB,
    private serviceC: ServiceC) {}

  ngOnInit() {
    this.subscriptionA = this.serviceA.subscribe(...);
    this.subscriptionB = this.serviceB.subscribe(...);
    this.subscriptionC = this.serviceC.subscribe(...);
  }

  ngOnDestroy() {
    this.subscriptionA.unsubscribe();
    this.subscriptionB.unsubscribe();
    this.subscriptionC.unsubscribe();
  }

}

Cela fonctionne, mais c'est un peu redondant. Je n'aime surtout pas cela - La unsubscribe() est ailleurs, alors vous devez vous rappeler que celles-ci sont liées. - L'état du composant est pollué avec l'abonnement.

Je préférerais de beaucoup utiliser l'opérateur takeUntil() ou quelque chose de similaire, pour lui donner ceci:

class myComponent {

  constructor(
    private serviceA: ServiceA,
    private serviceB: ServiceB,
    private serviceC: ServiceC) {}

  ngOnInit() {
    const destroy = Observable.fromEvent(???).first();
    this.subscriptionA = this.serviceA.subscribe(...).takeUntil(destroy);
    this.subscriptionB = this.serviceB.subscribe(...).takeUntil(destroy);
    this.subscriptionC = this.serviceC.subscribe(...).takeUntil(destroy);
  }

}

Existe-t-il un événement destroy ou quelque chose de similaire qui me permettrait d'utiliser takeUntil() ou un autre moyen de simplifier l'architecture de composant de cette manière? Je réalise que je pourrais créer moi-même un événement dans le constructeur ou quelque chose qui se déclenche dans ngOnDestroy(), mais cela ne rendrait finalement pas les choses beaucoup plus simples à lire.

29
JeffreyHammansson

Vous pouvez utiliser un ReplaySubject pour cela:

EDIT: Différent depuis RxJS 6.x: Notez l'utilisation de la méthode pipe().

class myComponent {
  private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);

  constructor(
    private serviceA: ServiceA,
    private serviceB: ServiceB,
    private serviceC: ServiceC) {}

  ngOnInit() {
    this.serviceA
      .pipe(takeUntil(this.destroyed$))
      .subscribe(...);
    this.serviceB
      .pipe(takeUntil(this.destroyed$))
      .subscribe(...);
    this.serviceC
      .pipe(takeUntil(this.destroyed$))
      .subscribe(...);
  }

  ngOnDestroy() {
    this.destroyed$.next(true);
    this.destroyed$.complete();
  }
}

Ceci n’est valable que pour RxJS 5.x et les versions antérieures:

class myComponentOld {
  private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);

  constructor(private serviceA: ServiceA) {}

  ngOnInit() {
    this.serviceA
      .takeUntil(this.destroyed$)
      .subscribe(...);
  }

  ngOnDestroy() {
    this.destroyed$.next(true);
    this.destroyed$.complete();
  }
}
45
olsn

Utiliser la fonction componentDestroyed() du paquet npm ng2-rx-composantdestroyed est de loin le moyen le plus simple d'utiliser takeUntil:

@Component({
  selector: 'foo',
  templateUrl: './foo.component.html'
})
export class FooComponent implements OnInit, OnDestroy {
  ngOnInit() {
    Observable.interval(1000)
      .takeUntil(componentDestroyed(this)) // <--- magic is here!
      .subscribe(console.log);
  }

  ngOnDestroy() {}
}

Voici une version de componentDestroyed() à inclure directement dans votre code:

// Based on https://www.npmjs.com/package/ng2-rx-componentdestroyed
import { OnDestroy } from '@angular/core';
import { ReplaySubject } from 'rxjs/ReplaySubject';

export function componentDestroyed(component: OnDestroy) {
  const oldNgOnDestroy = component.ngOnDestroy;
  const destroyed$ = new ReplaySubject<void>(1);
  component.ngOnDestroy = () => {
    oldNgOnDestroy.apply(component);
    destroyed$.next(undefined);
    destroyed$.complete();
  };
  return destroyed$;
}
10
Rene Hamburger

Eh bien, cela revient à ce que vous entendez par la fermeture d’un abonnement. Il y a fondamentalement deux manières de faire ceci:

  1. Utilisation d'un opérateur complétant la chaîne (tel que takeWhile()).
  2. Se désabonner de la source Observable.

Il est bon de savoir que ces deux ne sont pas les mêmes.

Lorsque vous utilisez, par exemple, takeWhile(), vous obligez l'opérateur à envoyer complete une notification qui est propagée à vos observateurs. Donc si vous définissez:

...
.subscribe(..., ..., () => doWhatever());

Ensuite, lorsque vous complétez la chaîne avec par exemple. takeWhile() la fonction doWhatever() sera appelée.

Par exemple, cela pourrait ressembler à ceci:

const Observable = Rx.Observable;
const Subject = Rx.Subject;

let source = Observable.timer(0, 1000);
let subject = new Subject();

source.takeUntil(subject).subscribe(null, null, () => console.log('complete 1'));
source.takeUntil(subject).subscribe(null, null, () => console.log('complete 2'));
source.takeUntil(subject).subscribe(null, null, () => console.log('complete 3'));

setTimeout(() => {
  subject.next();
}, 3000);

Après 3 secondes, tous les rappels complets seront appelés.

D'autre part, lorsque vous vous désabonnez, vous dites que les éléments produits par la source Observable ne vous intéressent plus. Cependant, cela ne signifie pas que la source doit être complétée. Vous ne vous en souciez plus.

Cela signifie que vous pouvez collecter tous les appels Subscriptions de .subscribe(...) et les désabonner tous à la fois:

let subscriptions = new Rx.Subscription();
let source = Observable.timer(0, 1000);

subscriptions.add(source.subscribe(null, null, () => console.log('complete 1')));
subscriptions.add(source.subscribe(null, null, () => console.log('complete 2')));
subscriptions.add(source.subscribe(null, null, () => console.log('complete 3')));

setTimeout(() => {
  subscriptions.unsubscribe();
}, 3000);

Maintenant, après 3 secondes de retard, rien ne sera imprimé sur la console car nous nous sommes désabonnés et aucun rappel complet n'a été appelé.

Donc, ce que vous voulez utiliser dépend de vous et de votre cas d'utilisation. Sachez simplement que la désinscription n’est pas la même chose que l’aboutissement même si, dans votre cas, cela n’a pas d’importance.

10
martin

Si votre composant est directement lié à une route, vous pouvez éviter d'ajouter un état en utilisant Router events avec takeUntil() . Ainsi, dès que vous éloignez le composant, il nettoie automatiquement ses abonnements.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MyService } from './my.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/takeUntil';

@Component({
    ...
})
export class ExampleComopnent implements OnInit {

    constructor(private router: Router, private myService: MyService) {
    }

    ngOnInit() {
        this.myService.methodA()
            .takeUntil(this.router.events)
            .subscribe(dataA => {
                ...
            });

        this.myService.methodB()
            .takeUntil(this.router.events)
            .subscribe(dataB => {
                ...
            });
    }
}

Remarque: Cet exemple simple ne prend pas en compte les itinéraires gardés ou la navigation annulée. S'il existe un risque que l'un des événements du routeur soit déclenché mais que la navigation sur un itinéraire soit annulée, vous devez filtrer les événements du routeur afin qu'il soit déclenché au moment approprié - par exemple, après le Route Guard ou une fois la navigation terminée.

this.myService.methodA()
    .takeUntil(this.router.events.filter(e => e instanceOf NavigationEnd))
    .subscribe(dataA => {
        ...
    });
1
Matt