angular
March 26

Оператор takeUntilDestroy() в Angular

Еще в Angular 16 появился оператор takeUntilDestroy() для RxJS подписок. Он существенно облегчает процесс работы с Observable.

При подписке на Observable разработчик должен иметь в виду необходимость отписок при уничтожении компонента, чтобы предотвратить утечки памяти. Существует несколько методов, и все они требуют написания лишнего кода.

Например, у нас есть подписка на сервис:

export class Component implements OnInit {
  data;
 
  ngOnInit(): void {
    this.service.getData().subscribe(
	    response => this.data = response.
    )
  }
}

И отписка от него может быть реализована через takeUntil() и subject:

export class Component implements OnInit, OnDestroy {
  data;
  destroyed = new Subject()
 
  ngOnInit(): void {
    this.service.getData()
      .pipe(
        takeUntil(this.destroyed),
      )
      .subscribe(
        response => this.data = response
      )
  }
 
  ngOnDestroy(): void {
    this.destroyed.next();
    this.destroyed.complete();
  }
}


Injectable OnDestroy

В Angular 16 появилась возможность не наследовать компонент от ngOnDestroy() и просто прокинуть DestroyRef

destroyRef = inject(DestroyRef);

В результате чего пример с takeUntil()может быть изменен следующим образом:

export class Component implements OnInit {
  destroyRef = inject(DestroyRef);
 
  ngOnInit(): void {
    const destroyed = new Subject();
 
    this.destroyRef.onDestroy(() => {
      destroyed.next();
      destroyed.complete();
    });
 
    this.service.getData()
      .pipe(takeUntil(destroyed))
      .subscribe(response => this.data = response)
  }
}

Команда Angular на этом не остановилась, и предложила свою реализацию для использования в пайпах.


takeUntilDestroy

Вопрос отписок можно упростить используя оператор takeUntilDestroy. Просто добавьте его в pipe без передачи каких-либо аргументов, и он автоматически выберет правильный OnDestroy для текущего контекста — используя инъекцию OnDestroy.

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 
export class Component implements OnInit{
  data;
 
  constructor(private service: DataService) {
    this.service.getData()
      .pipe(takeUntilDestroyed())
      .subscribe(response => this.data = response)
  }
}

В большинстве случаев это все что нужно сделать.

В случае, если требуется обработать событие уничтожения другого компонента, takeUntilDestroyed() может принять аргументом DestroyRef

Например: у родительского компонента есть подписка которая должна оставаться активной до тех пор, пока дочерний компонент показан на экране. Для этого можно внедрить DestroyRef в дочерний компонент:

export class Child {
  destroyRef = inject(DestroyRef);
}


Мы можем использовать новый оператор takeUntilDestroyed в родительском компоненте, чтобы закрыть подписку, передавая ссылку на DestroyRef дочернего компонента. Вот пример реализации для родительского компонента:

export class Parent {
  @ViewChild(Child) child: Child;
 
  ngOnInit(): void {
     interval(1000)
       .pipe(takeUntilDestroyed(this.child.destroyRef))
       .subscribe((count) => console.log(count));
  }
}

Пока компонент Child существует, значение числа из интервала будет попадать в консоль. После его уничтожения подписка в родительском компоненте будет остановлена.