Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router - Clarify params subscription behavior when deactivating a route #16261

Closed
NetanelBasal opened this issue Apr 23, 2017 · 22 comments
Closed
Assignees
Labels
area: router freq3: high P4 A relatively minor issue that is not relevant to core functions
Milestone

Comments

@NetanelBasal
Copy link

NetanelBasal commented Apr 23, 2017

[ x ] bug report

Current behavior
This is a quote from the official documentation -

The subscriptions are cleaned up when the component is destroyed, protecting against memory leaks, so you don't need to unsubscribe from the route params Observable.

The subscription is still active.

Expected behavior
Angular should unsubscribe from the params observable when the component destroyed. ( or change the documentation )

Minimal reproduction of the problem with instructions
You can test this behavior with the delay operator.

@Component({
 ...
})
export class TestComponent implements OnInit {
  params;
  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.params = this.route.params.delay(3000).subscribe(console.log);
  }
}

When navigates to a different route the handler is still running.

You can verify this by calling explicitly to the unsubscribe() method.

ngOnDestroy() {
  this.params.unsubscribe();
}

Now you will see that the handler is not running.

Angular version: 4.0.X

@mhevery
Copy link
Contributor

mhevery commented Apr 24, 2017

@robwormald Can you comment on how you think this should work?

@tytskyi
Copy link

tytskyi commented Jun 2, 2017

@mhevery @robwormald ping

@sod
Copy link
Contributor

sod commented Jun 9, 2017

Plunkr https://plnkr.co/edit/ftZj8PPYV5lovmgpRXAT?p=preview

Seems not to work like documented in 2.0.0, 4.0.0 and 4.2.0. All subscriptions remain.

@ribizli
Copy link

ribizli commented Jun 10, 2017

The ActivatedRoute and its observables are insulated from the Router itself. The Router destroys a routed component when it is no longer needed and the injected ActivatedRoute dies with it.

My understanding of this is that the source params/data/etc observables are garbage collected when the component and its injector with the ActivatedRoute gets garbage collected. No wording about unsubscribe or any cleanup.
It seems, that a delayed Observable still holds an active reference for a timeout callback or subscriber (??? I'm not aware about the internals) preventing it from garbage collection. After the timeout fires (or in real scenarios the HTTP returns), the Observable dies. Anyway I don't see this as a memory leak, rather just a specific case what should be handled manually.
But true, this should be well documented, since some HTTP call should be also cancelled to prevent from some side effects and save some resources.

Can someone confirm my assumptions?

@richarddavenport
Copy link

@sod @NetanelBasal Looking at that plnkr, it looks like only one message is being logged when switching between the XComponent and YComponent. This is with 4.2.0, can either of you confirm that using that plnkr there is a problem?
kapture 2017-09-15 at 11 24 13

@NetanelBasal
Copy link
Author

Yes, there is a problem, when you unsubscribe from the Observable the handler should NOT run.

@bennadel
Copy link

I just stumbled upon a slightly different problem, kind of opposite. I am seeing very strange routable component destroy/re-create if I don't include the .delay() option in the param subscription. But, not in all cases. Trying to get a reproduction.

@richarddavenport
Copy link

@bennadel are you saying that the component is being destroyed, but then recreated when the params are changed? But it only occurs when you don't include the delay?

@bennadel
Copy link

@richarddavenport yes, that is correct. As far as I have isolated, it appears to be when I have a synchronous navigation away from the component from within the paramMap.subscribe() callback. I suspect that by adding the .delay(0), it provides a ticket between the paramMap and my navigation event.

I hope to a working, isolated demo later today.

@bennadel
Copy link

bennadel commented Oct 27, 2017

@richarddavenport sorry, after further digging, it appears my issue has nothing to do with the ActivatedRoute at all. There is some race condition if you navigate away from the component too close to the ngOnInit() call. I wrote it up on my blog. Sorry for the distraction :)

I also created a new Ticket: #19999

... it seems to only affect Chrome.

@bennadel
Copy link

Ok, after more noodling, I think my issue does have to do with ActivatedRoute, because the behavior can be shown even when I navigate away from a component that has been on the page for a long time.

@tproenca
Copy link

tproenca commented Dec 6, 2017

The router is not auto unsubscribing for params or queryParams only when I have more than 1 instance of the same component being rendered in the page.

@LucasFrecia
Copy link

LucasFrecia commented Mar 8, 2018

Any update on this issue?

I had to do a custom fix for this that adds some overhead but got the job done.... check the stack overflow link, I leave it here for anyone who needs a quick efficient fix for this:

https://stackoverflow.com/questions/49159416/url-change-not-firing-in-angular/49159722#49159722

@m0uneer
Copy link

m0uneer commented Aug 7, 2018

I'm searching for a while for the best practices of unsubscribing from observables at Angular and I decided to share with you the result of my searches to review it if I miss something. I see that this issue is related and decided to comment here instead of creating a question issue.

Some of the best practices regarding observables unsubscriptions inside Angular components:

A quote from Routing & Navigation

When subscribing to an observable in a component, you almost always arrange to unsubscribe when the component is destroyed.

There are a few exceptional observables where this is not necessary. The ActivatedRoute observables are among the exceptions.

The ActivatedRoute and its observables are insulated from the Router itself. The Router destroys a routed component when it is no longer needed and the injected ActivatedRoute dies with it.

Feel free to unsubscribe anyway. It is harmless and never a bad practice.

And in responding to the following links:

I collected some of the best practices regarding observables unsubscriptions inside Angular components to share with you:

  • Never unsubscribe from an http observable as it is finite and it unsubscribes and cleans itself automatically (1), (2)
    http observable unsubscription is conditional and we should consider the effects of the 'subscribe callback' being run after the component is destroyed on a case by case basis. We know that angular unsubscribes and cleans the http observable itself (1), (2). While this is true from the perspective of resources it only tells half the story. Let's say we're talking about directly calling http from within a component, and the http response took longer than needed so the user closed the component. The subscribe() handler will still be called even if the component is closed and destroyed. This can have unwanted side effects and in the worse scenarios leave the application state broken. It can also cause exceptions if the code in the callback tries to call something that has just been disposed of. However at the same time occasionally they are desired. Like, let's say you're creating an email client and you trigger a sound when the email is done sending - well you'd still want that to occur even if the component is closed (8).
  • No need to unsubscribe from observables that complete or error. However, there is no harm in doing so(7).
  • Use AsyncPipe as much as possible because it automatically unsubscribes from the observable on component destruction.
  • Unsubscribe from the ActivatedRoute observables like route.params if they are subscribed inside a nested (Added inside tpl with the component selector) or dynamic component as they may be subscribed many times as long as the parent/host component exists. No need to unsubscribe from them in other scenarios as mentioned in the quote above from Routing & Navigation docs.
  • Unsubscribe from global observables shared between components that are exposed through an Angular service for example as they may be subscribed multiple times as long as the component is initialized.
  • No need to unsubscribe from internal observables of an application scoped service since this service never get's destroyed, unless your entire application get's destroyed, there is no real reason to unsubscribe from it and there is no chance of memory leaks. (6).

    Note: Regarding scoped services, i.e component providers, they are destroyed when the component is destroyed. In this case, if we subscribe to any observable inside this provider, we should consider unsubscribing from it using the OnDestroy lifecycle hook which will be called when the service is destroyed, according to the docs.
  • Use an abstract technique to avoid any code mess that may be resulted from unsubscriptions. You can manage your subscriptions with takeUntil (3) or you can use this npm package mentioned at (4) The easiest way to unsubscribe from Observables in Angular.
  • Always unsubscribe from FormGroup observables like form.valueChanges and form.statusChanges
  • Always unsubscribe from observables of Renderer2 service like renderer2.listen
  • Unsubscribe from every observable else as a memory-leak guard step until Angular Docs explicitly tells us which observables are unnecessary to be unsubscribed (Check issue: (5) Documentation for RxJS Unsubscribing (Open)).
  • Bonus: Always use the Angular ways to bind events like HostListener as angular cares well about removing the event listeners if needed and prevents any potential memory leak due to event bindings.

A nice final tip: If you don't know if an observable is being automatically unsubscribed/completed or not, add a complete callback to subscribe(...) and check if it gets called when the component is destroyed.

@simeyla
Copy link

simeyla commented Aug 15, 2018

@m0uneer I very much disagree with

Never unsubscribe from an http observable as it is finite and it unsubscribes and cleans itself automatically

While this is true from the perspective of resources it only tells half the story. Let's say we're talking about directly calling http from within a component, and the http response took longer than needed so the user closed the component. The subscribe() handler will still be called even if the component is closed and destroyed. This can have unwanted side effects and in the worse scenarios leave the application state broken. It can also cause exceptions if the code in the callback tries to call something that has just been disposed. However at the same time occasionally they are desired. Like let's say you're creating an email client and you trigger a sound when the email is done sending - well you'd still want that to occur even if the component is closed.

The point being never say never, and consider the effects of the 'subscribe callback' being run after the component is destroyed on a case by case basis. It may be better to 'unsubscribe', or it may be better to run the logic with some additional if (this._isDestoyed) {...} logic.

@m0uneer
Copy link

m0uneer commented Aug 16, 2018

@simeyla, thanks for giving the time to review my comment and regarding the http observable, I agree with you :) and I edited my comment to include your point of view.

@raysuelzer
Copy link

I am not sure if this is related, but I've noticed that ngOnDestroy() is called after the nagivation events, which in my case means that anything subscribed to queryParams will be called when the page navigates.

@viperphase1
Copy link

viperphase1 commented Feb 8, 2020

I just noticed this too. I built my subscriptions on the assumption that ngOnDestroy would be called before ActivatedRoute.params pushes out another change and now I'm having problems.

@hahn-kev
Copy link

Looks like the original issue here is still a problem. Here's a reproduction in StackBlitz, if you open up the console you will see if you switch between the login page and something else, you will see that subscriptions keep getting created, but the subscriptions are never completed or unsubscribed. However per the docs here it says:

When subscribing to an observable in a component, you almost always arrange to unsubscribe when the component is destroyed.

There are a few exceptional observables where this is not necessary. The ActivatedRoute observables are among the exceptions.

The ActivatedRoute and its observables are insulated from the Router itself. The Router destroys a routed component when it is no longer needed and the injected ActivatedRoute dies with it.

Feel free to unsubscribe anyway. It is harmless and never a bad practice.

however it seems that is not true based on the above tests. Can someone confirm that I'm doing the right thing here to test this?

@jelbourn jelbourn added P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent and removed severity3: broken labels Oct 1, 2020
@Andrei0872
Copy link

@hahn-kev I'm pretty sure there will be no memory leaks. That's because the entity, in this case a BehaviorSubject(ActivatedRoute's properties are BehaviorSubject instances), is destroyed when the ActivatedRoute associated to that view is destroyed.

I've detailed here why memory leaks occur when using a Subject. From my perspective, the Subject holds that subscribe's callback(it holds all the subscribers in a list) and when you unsubscribe, that subscriber(which indirectly links to the callback) is removed from the list. But if the thing that holds everything is destroyed, then I think there will be no memory leaks.

@atscott
Copy link
Contributor

atscott commented Dec 18, 2020

This appears to be working as intended and as @Andrei0872 noted, it isn't a memory leak. It doesn't appear that the documentation is necessarily misleading, it's just that rxjs behavior is difficult to understand. A couple important things to note:

  • The component is destroyed, meaning that all of its members will be destroyed as well, including any subscriptions to the route params
  • This does not mean that any finalize operators or complete on the subscription is called. This only happens if the source completes or you unsubscribe from the subscription. The documentation doesn't state that the Router will do this, though I can still understand why the behavior is confusing.

You can play around with these scenarios here:
https://stackblitz.com/edit/rxjs-hmpizx?devtoolsheight=60

  • complete the source without unsubscribe: next, complete and finalize are all called, even when complete is called before the next from the delay
  • unsubscribe from subscription without complete on the source: finalize happens, but the complete of the subscription does not, and neither does next

So even if the Router were to call complete on all of the Observables on an ActivatedRoute, you would see that any subscriptions with a delay operator would still get called.

I'm lowering the priority here since I believe this is working as intended but that the documentation can be clarified to recommend unsubscribing if you need/desire the subscriptions to complete and/or finalize.

@atscott atscott added comp: docs P4 A relatively minor issue that is not relevant to core functions and removed P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent state: needs more investigation type: bug/fix labels Dec 18, 2020
@atscott atscott changed the title Router - auto unsubscribe from the params observable is not working Router - Clarify params subscription behavior when deactivating a route Dec 18, 2020
atscott added a commit to atscott/angular that referenced this issue May 25, 2021
angular#16261 (comment)

A couple important things to note about the behavior:

* The component is destroyed, meaning that all of its members will be destroyed as well, including any subscriptions to the route params
* This _does not_ mean that any `finalize` operators or `complete` on the subscription is called. This only happens if the source completes or you unsubscribe from the subscription. The documentation doesn't state that the `Router` will do this, though I can still understand why the behavior is confusing.

You can play around with these scenarios here:
https://stackblitz.com/edit/rxjs-hmpizx
* `complete` the source without unsubscribe: `next`, `complete` and `finalize` are all called, even when `complete` is called before the `next` from the `delay`
* `unsubscribe` from subscription without `complete` on the source: `finalize` happens, but the `complete` of the subscription does not, and neither does `next`

So even if the `Router` were to call `complete` on all of the `Observables` on an `ActivatedRoute`, you would see that any subscriptions with a `delay` operator would still get called.

resolves angular#16261
@atscott atscott self-assigned this May 25, 2021
@zarend zarend closed this as completed in 29e5d50 May 25, 2021
zarend pushed a commit that referenced this issue May 25, 2021
#16261 (comment)

A couple important things to note about the behavior:

* The component is destroyed, meaning that all of its members will be destroyed as well, including any subscriptions to the route params
* This _does not_ mean that any `finalize` operators or `complete` on the subscription is called. This only happens if the source completes or you unsubscribe from the subscription. The documentation doesn't state that the `Router` will do this, though I can still understand why the behavior is confusing.

You can play around with these scenarios here:
https://stackblitz.com/edit/rxjs-hmpizx
* `complete` the source without unsubscribe: `next`, `complete` and `finalize` are all called, even when `complete` is called before the `next` from the `delay`
* `unsubscribe` from subscription without `complete` on the source: `finalize` happens, but the `complete` of the subscription does not, and neither does `next`

So even if the `Router` were to call `complete` on all of the `Observables` on an `ActivatedRoute`, you would see that any subscriptions with a `delay` operator would still get called.

resolves #16261

PR Close #42316
umairhm pushed a commit to umairhm/angular that referenced this issue May 28, 2021
…42316)

angular#16261 (comment)

A couple important things to note about the behavior:

* The component is destroyed, meaning that all of its members will be destroyed as well, including any subscriptions to the route params
* This _does not_ mean that any `finalize` operators or `complete` on the subscription is called. This only happens if the source completes or you unsubscribe from the subscription. The documentation doesn't state that the `Router` will do this, though I can still understand why the behavior is confusing.

You can play around with these scenarios here:
https://stackblitz.com/edit/rxjs-hmpizx
* `complete` the source without unsubscribe: `next`, `complete` and `finalize` are all called, even when `complete` is called before the `next` from the `delay`
* `unsubscribe` from subscription without `complete` on the source: `finalize` happens, but the `complete` of the subscription does not, and neither does `next`

So even if the `Router` were to call `complete` on all of the `Observables` on an `ActivatedRoute`, you would see that any subscriptions with a `delay` operator would still get called.

resolves angular#16261

PR Close angular#42316
iRealNirmal pushed a commit to iRealNirmal/angular that referenced this issue Jun 4, 2021
…42316)

angular#16261 (comment)

A couple important things to note about the behavior:

* The component is destroyed, meaning that all of its members will be destroyed as well, including any subscriptions to the route params
* This _does not_ mean that any `finalize` operators or `complete` on the subscription is called. This only happens if the source completes or you unsubscribe from the subscription. The documentation doesn't state that the `Router` will do this, though I can still understand why the behavior is confusing.

You can play around with these scenarios here:
https://stackblitz.com/edit/rxjs-hmpizx
* `complete` the source without unsubscribe: `next`, `complete` and `finalize` are all called, even when `complete` is called before the `next` from the `delay`
* `unsubscribe` from subscription without `complete` on the source: `finalize` happens, but the `complete` of the subscription does not, and neither does `next`

So even if the `Router` were to call `complete` on all of the `Observables` on an `ActivatedRoute`, you would see that any subscriptions with a `delay` operator would still get called.

resolves angular#16261

PR Close angular#42316
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Jun 25, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: router freq3: high P4 A relatively minor issue that is not relevant to core functions
Projects
None yet
Development

Successfully merging a pull request may close this issue.