DEV Community

George Knap
George Knap

Posted on

takeUntilDestroyed() - How I got disappointed

Hi Friends, George here.

It was a big surprise for me - takeUntilDestroyed - finally something natively supported by Angular coming to version 16.

When it arrived I was disapponted. Why?
Well let's say I was hoping for something more seamless to use.

Let's have a look at how this new pipe is implemented:

export function takeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperatorFunction<T> {
  if (!destroyRef) {
    assertInInjectionContext(takeUntilDestroyed);
    destroyRef = inject(DestroyRef);
  }

  const destroyed$ = new Observable<void>(observer => {
    const unregisterFn = destroyRef!.onDestroy(observer.next.bind(observer));
    return unregisterFn;
  });

  return <T>(source: Observable<T>) => {
    return source.pipe(takeUntil(destroyed$));
  };
}
Enter fullscreen mode Exit fullscreen mode

What is that weird destroyRef parameter?

DestroyRef is an injectable token which can take a callback onDestroy and this callback will be executed right before it's scope is destroyed. Angular needs this reference object to know when the to clean up your sbscription.

And that's the little annoying thing about this pipe. It's not magic! It doesn't work by itself! It always needs to have this destroyRef object.

How does it get it? There are two ways you can see in the code:

 if (!destroyRef) {
    assertInInjectionContext(takeUntilDestroyed);
    destroyRef = inject(DestroyRef);
  }
Enter fullscreen mode Exit fullscreen mode
  1. You can provide destroyRef manually as a pipe parameter

  2. You can let Angular to inject it by itself

This second option seems easy, right? No, because you need to be in the injection context.

Wait a sec, don't we have injection context everywhere?

No.

Here's where injection context works:

inject() must be called from an injection context 
such as a constructor, a factory function, a field initializer, 
or a function used with `runInInjectionContext`. 
Enter fullscreen mode Exit fullscreen mode

Get it?
You want to subscribe to an observable in ngOnInit and use takeUntilDestroyed to handle unsubscribe? Tough luck! You're not in the injection context.

How do we do it then?

We do it by injecting destroyRef in the injection context and passing it as a parameter to the pipe.
Example:

class MyComponent {
  destroyRef = inject(DestroyRef);
  ngOnInit() {
    timer(0, 1000).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(n => console.log('timer', n));
  }
}
Enter fullscreen mode Exit fullscreen mode

And here we are. The amount of code to do it is now quite similar to well known method of declaring an unsubscribe subject by yourself.

As we see this is not magic solution to all world's problems. In fact we should still keep in mind to try not to subscribe manually.

I was really looking forward to a future to discard our typescript mixin that handles unsubscribe logic inside a base class that you have to extend if needed. I might share it with you in another post so stay tuned.

Enjoy coding

To learn more about DestroyRef provider I highly recommend this video from Dmytro on Decoded Fronted Youtube channel

Top comments (0)