Angular. Способы отписываться от Observable

Вместо тысячи слов

Руками через свойство класса

Одна подписка в одно свойство. Имеет смысл, когда необходимо переподписываться в ходе жизненного цикла компонента, например в сеттере или ngOnChanges.

@Component()
export class ManualComponent implements OnInit, OnDestroy {
  private subscription = Subscription.EMPTY;

  ngOnInit() {
    this.subscription = interval(1000).subscribe(observer('manual'));
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

Вложенные подписки в свойстве класса

При отписки родителя отписываются все наследники.

@Component()
export class AddComponent implements OnInit, OnDestroy {
  readonly subscription = new Subscription();

  ngOnInit() {
    this.subscription.add(interval(900).subscribe(observer('add 1')));
    this.subscription.add(interval(1200).subscribe(observer('add 2')));
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

Стоит отметить, что данный способ не подходит для случаев переподписки.

Массив подписок в свойстве класса

@Component()
export class ArrayComponent implements OnInit, OnDestroy {
  private subscriptionList: Subscription[] = [];
  set sub(sub: Subscription) {
    this.subscriptionList.push(sub);
  }

  ngOnInit() {
    this.subscriptionList.push(interval(1200).subscribe(observer('array 1')));
    // or
    this.sub = interval(900).subscribe(observer('array 2'));
  }

  ngOnDestroy(): void {
    this.subscriptionList.forEach(s => s.unsubscribe());
  }
}

takeUntil

Самый простой декларативный способ отписки

@Component()
export class TakeUntilComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    interval(900)
      .pipe(takeUntil(this.destroy$))
      .subscribe(observer('take-until 1'));
    interval(1200)
      .pipe(takeUntil(this.destroy$))
      .subscribe(observer('take-until 2'));
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

DestroyService

Вариация декларативного способа отписки от ребят из Тинькофф. Преимущество над простым способом в том, что не нужно каждый раз добавлять ngOnDestroy. Может помочь в ситуации, когда нужно отписаться внутри DI фабрики.

/**
 * @see https://github.com/TinkoffCreditSystems/taiga-ui
 * Observable abstraction over ngOnDestroy to use with takeUntil
 */
@Injectable()
export class DestroyService extends Subject<void> implements OnDestroy {
  ngOnDestroy() {
    this.next();
    this.complete();
  }
}

@Component({
  ...
  providers: [DestroyService]
})
export class DestroyServiceComponent implements OnInit {
  constructor(private destroy$: DestroyService) {}

  ngOnInit() {
    interval(900)
      .pipe(takeUntil(this.destroy$))
      .subscribe(observer('destroy-service 1'));
  }
}

UntilDestroy

https://github.com/ngneat/until-destroy Использую в своих приложениях и на работе. Сомнительный выбор для распространяемых библиотек так как лишняя зависимость.

Улучшает 3 варианта:

  • Подписка в свойстве
  • Подписки в массиве
  • takeUntil

Преимущество в том, что не нужно каждый раз добавлять ngOnDestroy

@UntilDestroy({ checkProperties: true })
@Component()
export class UntilDestroyedManualComponent implements OnInit {
  private subscription = Subscription.EMPTY;

  ngOnInit() {
    this.subscription = interval(1000).subscribe(
      observer('until-destroyed-manual')
    );
    this.subscription.add(
      interval(1200).subscribe(observer('until-destroyed-add'))
    );
  }
}
@UntilDestroy({ arrayName: 'subscriptionList' })
@Component()
export class UntilDestroyedArrayComponent implements OnInit {
  private subscriptionList: Subscription[] = [];
  set sub(sub: Subscription) {
    this.subscriptionList.push(sub);
  }

  ngOnInit() {
    this.subscriptionList.push(
      interval(1200).subscribe(observer('until-destroyed-array 1'))
    );
    // or
    this.sub = interval(900).subscribe(observer('until-destroyed-array 2'));
  }
}
@UntilDestroy()
@Component()
export class UntilDestroyedComponent implements OnInit {
  ngOnInit() {
    interval(900)
      .pipe(untilDestroyed(this))
      .subscribe(observer('until-destroyed 1'));
    interval(1200)
      .pipe(untilDestroyed(this))
      .subscribe(observer('until-destroyed 2'));
  }
}

Вместо послесловия

Если можно отдать подписку в шаблон, то лучше так и сделать. Если нельзя, то по возможности сделать декларативно, в приложении через UntilDestroy, в библиотеке - через takeUntil и его вариации. Для случаев переподписки подойдет любой вариант. И не забывать, что в providedIn: 'root' сервисах нельзя полагаться на вызов ngOnDestroy.

Похожие записи

Об subscribe() vs async

О предпочтительности использования async pipe. При OnPush стратегии не требуется вызывать markForCheck() внутри подписки +решение с несколькими | async pipes развёрнутых в одну переменную (внутри шаблона).

05 января 2019 г. в Angular

Angular. Отличие baseHref от deployUrl

  • deployUrl - задаёт путь для статических (js, css) файлов в index.html.
  • baseHref - определяет base, используется в ссылках и маршрутизации (routing) Angular