18

To avoid Observable memory leaks inside Components, I am using takeUntil() operator before subscribing to Observable.

I write something like this inside my components:

private unsubscribe$ = new Subject();

ngOnInit(): void {
  this.http
    .get('test')
    .pipe(takeUntil(this.unsubscribe$))
    .subscribe((x) => console.log(x));
}

ngOnDestroy(): void {
  this.unsubscribe$.next();
  this.unsubscribe$.complete(); // <--- ??
}

Finally my question is following:

Do I need to write this.unsubscribe$.complete(); or next() is enough?

Is unsubscribe$ going to be grabbed by garbage collector without completing?

Please explain is there difference or doesn't matter. I don't want memory leaks in my components.

Goga Koreli
  • 2,807
  • 1
  • 12
  • 31
  • Next is enough if you’re using this particular method – bryan60 Jul 12 '19 at 12:55
  • @bryan60 Can you explain why? Thanks – Goga Koreli Jul 12 '19 at 13:12
  • 1
    explanation was long so I wrote an answer explaining what causes leaks and why. – bryan60 Jul 12 '19 at 15:29
  • @bryan60 , I understand why in this particular method we don't have to call this.unsubscribe$.complete(); , but what if we subscribe to an observable from a service provided In Root and the observable never complete ? – Borneo777 Oct 21 '21 at 14:58
  • @borneo777 that would create a memory leak if the observable doesn't complete or you never unsubscribe and the subscribing component gets destroyed. see my answer for more. – bryan60 Oct 21 '21 at 15:37
  • @bryan60 I suppose you haven't understood described case. I am talking about exactly the same example as described in post, but without line "this.unsubscribe$.complete();" and with subscription to the service observable (instead of "this.http.get") I think, that in such situation, memory leak won't appear on component descruction, because once unsubscribe$ emits a value, takeUntil unscubscribe from both service observable and unsubscribe$. – Borneo777 Oct 21 '21 at 16:02
  • you don't need to call complete. – bryan60 Oct 21 '21 at 16:04

2 Answers2

32

Short answer, no this is not needed, but it also doesn't hurt.

Long answer:

Unsubscribing / completing in angular is only needed when it prevents garbage collection because the subscription involves some subject that will outlive the component due to be collected. This is how a memory leak is created.

if I have service:

export class MyService {
  mySubject = new Subject();
}

which is provided in root and only root (which means it's a singleton and will never be destroyed after instantiation) and a component that injects this service and subscribes to it's subject

export class MyLeakyComponent {
  constructor(private myService: MyService) {
    this.myService.mySubject.subscribe(v => console.log(v));
  }
}

this is creating a memory leak. Why? because the subscription in MyLeakyComponent is referenced by the subject in MyService, so MyLeakyComponent can't be garbage collected so long as MyService exists and holds a reference to it, and MyService will exist for the life of the application. This compounds everytime you instantiate MyLeakyComponent. To fix this, you must either unsubscribe or add a terminating operator in the component.

however this component:

export class MySafeComponent {
  private mySubect = new Subject();
  constructor() {
    this.mySubject.subscribe(v => console.log(v));
  }
}

is completely safe and will be garbage collected without issue. No external persisting entity holds a reference to it. This is also safe:

@Component({
  providers: [MyService]
})
export class MyNotLeakyComponent {
  constructor(private myService: MyService) {
    this.myService.mySubject.subscribe(v => console.log(v));
  }
}

now the injected service is provided by the component, so the service and the component will be destroyed together and can be safely garbage collected as the external reference will be destroyed as well.

Also this is safe:

export class MyHttpService { // root provided
  constructor(private http: HttpClient) {}
  
  makeHttpCall() {
    return this.http.get('google.com');
  }
}

export class MyHttpComponent {
  constructor(private myhttpService: MyHttpService) {
    this.myhttpService.makeHttpCall().subscribe(v => console.log(v));
  }
}

because http calls are a class of observables that are self terminating, so they terminate naturally after the call completes, so no need to manually complete or unsubscribe, as the external reference is gone once it naturally completes.

As to your example: the unsubscribe$ subject is local to the component, thus it cannot possibly cause a memory leak. This is true of any local subject.

A note on best practices: Observables are COMPLEX. One that might look completely safe, could involve an external subject in a subtle manner. To be totally safe / if you're not extremely comfortable with observables, it is generally recommended that you unsubscribe from all non terminating observables. There isn't a downside other than your own time spent doing it. I personally find the unsubscribe$ signal method hacky and think it pollutes / confuses your streams. the easiest to me is something like this:

export class MyCleanedComponent implements OnDestroy {
  private subs: Subscription[] = [];
  constructor(private myService: MyService) {
    this.subs.push(
      this.myService.mySubject.subscribe(v => console.log(v)),
      this.myService.mySubject1.subscribe(v => console.log(v)),
      this.myService.mySubject2.subscribe(v => console.log(v))
    );
  }
  
  ngOnDestroy() {
    this.subs.forEach(s => s.unsubscribe());
  }
}

However, the single BEST method for preventing leaks is using the async pipe provided by angular as much as possible. It handles all subscription management for you.

ry8806
  • 2,258
  • 1
  • 23
  • 32
bryan60
  • 28,215
  • 4
  • 48
  • 65
  • Thanks, this is what I wanted to read. I knew some bits what you told me here, but I was lacking in depth info. This is exactly what I wanted to see to finally understand Subjects, Observables and Memory leaks. – Goga Koreli Jul 12 '19 at 15:35
  • I agree, I strongly suggest Async pipe wherever it can possibly be used. – Goga Koreli Jul 12 '19 at 15:39
1

I can see that you are subscribing to a http response. There is one important thing when using Angular HttpClient: You don't have to unsubscribe from it as it is done automatically!

You can test that using finalize operator - it is called when observable completes. (And when observable completes, it is automatically unsubscribed)

this.http
  .get('test')
  .pipe(takeUntil(this.unsubscribe$), finalize(() => console.log('I completed and unsubscribed')))
  .subscribe((x) => console.log(x));

If you are worried that your component might die while the HTTP request is still on it's way, and that code in callback might still execute, you could do something like this:

private subscription: Subscription;

ngOnInit(): void {
  this.subscription = this.http
    .get('test')
    .subscribe((x) => console.log(x));
}

ngOnDestroy(): void {
  // Here you should also do a check if subscription exists because in most cases it won't
  this.subscription.unsubscribe();
}

It is also worth checking the AsyncPipe

The AsyncPipe subscribes (and unsubscribes) for you automatically.

Dino
  • 7,779
  • 12
  • 46
  • 85
  • Thanks for your time and nice tips. By the way I still would like to use `takeUntil()` operator, because it helps me to cancel the subscription when component dies (for example due to navigation to the different page). My question still remains though: What happens to the Subject? Does it get garbage collected? – Goga Koreli Jul 12 '19 at 13:10
  • I don't understand this part: "because it helps me to cancel the subscription when component dies". The subscription to HttpClient dies as soon as you trigger it and get the response back. – Dino Jul 12 '19 at 13:13
  • 1
    Sometimes I have certain methods to be executed inside `subscribe()` after I receive http response, for example navigate to certain page. However if my component dies when the http response is on its way back I don't want those extra stuff to be executed. `takeUntil()` operator helps me to cancel the subscription when component dies and whatever is inside subscribe won't be executed. – Goga Koreli Jul 12 '19 at 13:18
  • You still don't have to use takeUntil() for this. Check my updated answer – Dino Jul 12 '19 at 13:31