โ† Back to list

๐Ÿ’Ž Angular ESLint: no-implicit-take-until-destroyed rule

Did you know you can use ESLint to ensure that the takeUntilDestroyed operator is called within an Angular injection context?

Wojciech Trawiล„ski in JavaScript everyday ยท 2026-02-11 09:17 ยท 59 claps ยท 2.8 min read
#angular #web-development #javascript #typescript #eslint
Open on Medium โ†—
Wiki topics: ๐ŸŒ ยท Web Development

๐Ÿ’Ž Angular ESLint: no-implicit-take-until-destroyed rule

Did you know you can use ESLint to ensure that the takeUntilDestroyed operator is called within an Angular injection context?

When Angular made the inject function public, they introduced the concept of an injection context. In short, there is a fixed set of conditions under which you can call inject so it can implicitly obtain dependencies from an injector. Giving implicit access to the injector (and thus dependency injection) from within a function unlocked a lot of possibilities thanks to easy function composability.

By using inject together with DestroyRef, Angular introduced the takeUntilDestroyed operator, which allows you to implicitly unsubscribe from an RxJS stream when, for example, a component is destroyed. The key thing is that you must use takeUntilDestroyed within an injection context:

import { Component, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-user',
  template: `...`,
})
export class User implements OnInit {
  private readonly userService = inject(UserService);
  protected readonly userInfo$ = this.userService.getUserInfo().pipe(takeUntilDestroyed());

  ngOnInit() {
    this.userInfo$.subscribe((userInfo) => {
      // do sth with userInfo
    });
  }
}

If you fail to do so, you will get a runtime error:

import { Component, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-user',
  template: `...`,
})
export class User implements OnInit {
  private readonly userService = inject(UserService);

  ngOnInit() {
    this.userService
      .getUserInfo()
      // ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ runtime error
      .pipe(takeUntilDestroyed())
      .subscribe((userInfo) => {
        // do sth with userInfo
      });
  }
}

The downside is that you only discover this bug at runtime, when you launch your application. Fortunately, you can enable the no-implicit-take-until-destroyed ESLint rule to catch these issues immediately in your IDE or CI pipeline:

import { Component, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-user',
  template: `...`,
})
export class User implements OnInit {
  private readonly userService = inject(UserService);

  ngOnInit() {
    this.userService
      .getUserInfo()
      // ๐Ÿ‘ฎ ๐Ÿšจ
      // @angular-eslint/no-implicit-take-until-destroyed
      // `takeUntilDestroyed()` must be called with an explicit `DestroyRef` parameter 
      // when used outside of an injection context.
      .pipe(takeUntilDestroyed())
      .subscribe((userInfo) => {
        // do sth with userInfo
      });
  }
}

You can simply fix it by providing DestroyRef explicitly:

import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-user',
  template: `...`,
})
export class User implements OnInit {
  private readonly userService = inject(UserService);
  private readonly destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.userService
      .getUserInfo()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((userInfo) => {
        // do sth with userInfo
      });
  }
}

Enabling the rule is a big improvement, but it does not catch every scenario. For example, it is perfectly valid to use the operator inside a runInInjectionContext callback, but the rule still reports a problem here โ€” a false positive:

import { Component, inject, Injector, OnInit, runInInjectionContext } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-user',
  template: `...`,
})
export class User implements OnInit {
  private readonly userService = inject(UserService);
  private readonly injector = inject(Injector);

  ngOnInit() {
    runInInjectionContext(this.injector, () => {
      this.userService
        .getUserInfo()
        // ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ NO runtime error, false positive
        // @angular-eslint/no-implicit-take-until-destroyed
        // `takeUntilDestroyed()` must be called with an explicit `DestroyRef` parameter 
        // when used outside of an injection context.
        .pipe(takeUntilDestroyed())
        .subscribe((userInfo) => {
          // do sth with userInfo
        });
    });
  }
}

On the other hand, some cases are not reported at all, for example when the operator usage is hidden inside a helper function:

import { assertInInjectionContext, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';

export function useUserInfo(): Observable<UserInfo> {
  assertInInjectionContext(useUserInfo);

  return inject(UserService).getUserInfo().pipe(takeUntilDestroyed());
}
@Component({
  selector: 'app-user',
  template: `...`,
})
export class User implements OnInit {
  private readonly userService = inject(UserService);

  ngOnInit() {
   // ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ runtime error, NOT reported by ESLint
    useUserInfo().subscribe((userInfo) => {
      // do sth with userInfo
    });
  }
}

Itโ€™s definitely a good idea to use ESLint to guard correct takeUntilDestroyed usage, but be aware of its false positives and blind spots, especially with runInInjectionContext.

I hope you liked the โ€œgolden nuggetโ€ ๐Ÿ’Ž , thanks for reading! ๐Ÿ™‚


๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
post_id
9f6cc82b4472
slug
angular-eslint-no-implicit-take-until-destroyed-rule-9f6cc82b4472
url
https://medium.com/javascript-everyday/angular-eslint-no-implicit-take-until-destroyed-rule-9f6cc82b4472
canonical_url
https://medium.com/javascript-everyday/angular-eslint-no-implicit-take-until-destroyed-rule-9f6cc82b4472
author_url
https://medium.com/@wojtrawi
status
ok
fetched_at
2026-06-14 11:28:49