39

I'm trying to replace my AbstractComponent (all the boilerplate for unsubscribing) with Angular's new takeUntilDestroyed().

But am getting an error out of the gate.

NG0203: takeUntilDestroyed() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext. Find more at https://angular.io/errors/NG0203

The blog post says:

By default, this operator will inject the current cleanup context. For example, used in a component, it will use the component’s lifetime.

The docs confirm that injecting context is optional. And this indepth article shows its use in OnInit without context. Here's how I'm using it.

  public ngOnInit(): void {
    this.route.firstChild.paramMap.pipe(
      takeUntilDestroyed()
    ).subscribe((res: ParamMap) => {
      ...
    });

How can this be resolved?

10
  • The doc also very explicitely state you should use the destroyRef parameter to pass the current context when outside an injection context.
    – Salketer
    Commented May 16, 2023 at 14:23
  • 3
    OnInit is outside the injection context? Commented May 16, 2023 at 14:24
  • 3
    Yes, the component has already finished injecting when OnInit is fired. Use the constructor instead
    – Salketer
    Commented May 16, 2023 at 14:26
  • 1
    Ok, also, I thought placing feature-wide subscriptions in the constructor was bad practice but I cannot find any reference to back that up. Commented May 16, 2023 at 14:27
  • 2
    It is not bad practice, not much than it is in onInit quite frankly. Some problems you may face in the constructor is that the component and children are not fully initialized.
    – Salketer
    Commented May 16, 2023 at 14:29

2 Answers 2

82

You need to pass destroyRef whenever you use takeUntilDestroyed() outside of an injection context. You can think of injection context as a space in your code that runs before the instantiation of a class, in this case a component. The constructor of a class is an example of an injection context because the code inside the constructor runs before the class is instanciated. Another example are class field declarations, whenever you declare a field you must either asign it a value directly or assign it within the constructor, this is because the value must be known at instantiation, which tells us that this assignment happens within an injection context.

This ones work fine:

export class FooCmp implements OnInit {
    route = inject(ActivatedRoute);
    params$ = this.route.firstChild.paramMap.pipe(takeUntilDestroyed())

    ngOnInit() {
        this.params$.subscribe(res => {/* some operation */})
    }
}

export class BarCmp {
    constructor(private route: ActivatedRoute) {
        this.route.firstChild.paramMap
        .pipe(takeUntilDestroyed())
        .subscribe(res => {
            // some operation
        })
    }
}
export class BazCmp implements OnInit {
    route = inject(ActivatedRoute)
    destroyRef = inject(DestroyRef)

    ngOnInit() {
        this.route.firstChild.paramMap
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe(res => {/* some operation */})
    }

}

This one wont:

export class FooCmp implements OnInit {
    route = inject(ActivatedRoute);

    ngOnInit() {
        this.route.firstChild.paramMap.pipe(takeUntilDestroyed())
    }
}

Edit: Link to Angular's documentation about injection context: https://angular.io/guide/dependency-injection-context

2
  • 16
    I thought takeUntilDestroyed() is a little bit more intelligent...
    – Michi-2142
    Commented Aug 10, 2023 at 14:02
  • 2
    It wishes! But that's the best it can do. Commented Aug 15, 2023 at 23:57
1

If you're using a recent version of angular that has DestroyRef. here's what Im doing:

Its a heavily modified version I found from https://twitter.com/Enea_Jahollari/status/1630665586245242881?s=20

export const injectOnDestroy = () => {
  const destroyRef = inject(DestroyRef);
  return new SafeSubscriber(destroyRef);
}

class SafeSubscriber {
  constructor(private destroyRef: DestroyRef){
    destroyRef.onDestroy(() => {
      console.log('destroyed!!!!')
      this.destroyed$.next();
      this.destroyed$.complete();
      this._subscriptions.forEach((sub) => sub.unsubscribe());
      this._timeouts.forEach((id) => clearTimeout(id));
      this._intervals.forEach((id) => clearInterval(id));
    });
  }

  // TAKE UNTIL eg: observable$.pipe(takeUntil(this.onDestroy.destroyed$)).subscribe()
  destroyed$ = new ReplaySubject<void>(1);

  // TIMEOUT eg: this.onDestroy.killSub = someObj.subscribe();
  private _subscriptions: Subscription[] = [];
  set killSub(sub: Subscription) { this._subscriptions.push(sub); }

  // TIMEOUT eg: this.onDestroy.killTimeout = setTimeout(...);
  private _timeouts: any[] = [];
  set killTimeout(id: any) { this._timeouts.push(id); }

  // INTERVAL eg:  this.onDestroy.killInterval = setInterval(...);
  private _intervals: any[] = [];
  set killInterval(id: any) { this._intervals.push(id); }
}

Then... in my components etc...

// someComponent.ts
@Component({ ... })
export class SomeCompoonent{
   // pull it in
   private onDestroy = injectOnDestroy();

   doSomething() {
      // then
      someObs$.pipe(takeUntil(this.onDestroy.destroyed$)).subscribe();
      // or
      this.onDestroy.killSub = someObs$.subscribe();
      // or
      this.onDestroy.killTimeout = setTimeout();
      // or
      this.onDestroy.killInterval = setInterval();

   }
}
2
  • 8
    This seems just unnecessarily complex
    – Harvey
    Commented Dec 8, 2023 at 14:56
  • Humm... sure there is a bit of code... but if you you look at the usage.. its actually extremely not-complex. You can inject onDestroy and automatically kill any timeouts, intervals, or subscriptions with the one inject.
    – nawlbergs
    Commented Feb 21 at 17:56

Not the answer you're looking for? Browse other questions tagged or ask your own question.