Skip to content

Pipeable operator for unsubscribing from observables in the components #23038

Closed

Description

@smnbbrv

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

For every observable I consume in the components I need to manually unsubscribe from it on component destruction.

What does it mean:

  1. For every subscription I need to create a property on a component (or an array of those subscriptions)
  2. In ngOnDestroy I need to unsubscribe from all subscriptions above

Normally this is a little bit painful. That's why I have a helper library that creates a pipeable operator that tracks the component destruction and kinda extends standard takeUntil pipeable:

export function untilDestroyed<T>(componentInstance: any, destructorName = 'ngOnDestroy'): MonoTypeOperatorFunction<T> {
  if (!componentInstance[metaProperty]) {
    const originalDestructor = componentInstance[destructorName];

    if (!originalDestructor && aot) {
      console.warn('untilDestroyed limitation: ngOnDestroy is not present on the component which is a problem for AoT. See https://github.com/angular/angular/issues/16023 for more details');
    }

    componentInstance[metaProperty] = new Subject();

    componentInstance[destructorName] = function() {
      if (originalDestructor) {
        originalDestructor.call(componentInstance);
      }

      componentInstance[metaProperty].next();
      componentInstance[metaProperty].complete();
    };
  }

  return takeUntil(componentInstance[metaProperty]);
}

Everything is cool, it is really comfortable to use as

interval(1000).pipe(untilDestroyed(this)).subscribe(console.log.bind(console));

where this is a component instance.

But here comes AoT and forces to create an empty ngOnDestroy according to this issue #16023.

Expected behavior

  1. As long as now RxJS is very flexible with those pipeables, can we have a similar standard pipeable shipped by angular?
  2. Can we embed this pipeable under the hood to bind to the component destructor that one is not forced to write ngOnDestroy?

Minimal reproduction of the problem with instructions

What is the motivation / use case for changing the behavior?

I need to manually unsubscribe from every observable in ngOnDestroy. Even using the custom pipeable I still need to create an empty ngOnDestroy. This could be done in a way better manner

Environment


Angular version: >=5.0.0


Browser:
- [ ] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
For Tooling issues:
- Node version: XX  
- Platform:  

Others:

Activity

  1. added this to the needsTriage milestone on Mar 28, 2018
  2. bcandullo commented on Mar 28, 2018

    @bcandullo

    I dig that helper you've created. Have you tried creating a base component to handle this which would automatically invoke the ngOnDestroy for sub components? Pseudo example:

    export abstract class DestroyableComponent {
      protected subscriptions: Subscription[] = [];
      protected subscribe(sub: Subscription[]) {
        this.subscriptions.push(sub);
      }
      protected ngOnDestroy() {
        this.subscriptions.forEach(s => s.unsubscribe());
      }
    }
    
    class MyComponent extends DestroyableComponent {
      constructor() {
        super();
        this.subscribe(service.getObservable());
      }
      // ngOnDestroy .. who needs it?
    }
    
  3. smnbbrv commented on Mar 28, 2018

    @smnbbrv
    Author

    hi @bcandullo

    this definitely works, however it has some disadvantages.

    1. This forces the super() call (so, the ngOnDestroy with empty body already pays off if you count the lines of code)
    2. this.subscribe(x) pushes the attention away from the meaningful part of the code (the actual x). This is especially visible when there is an observable with multiple pipeables which most likely takes more than one line
    3. In case when one needs ngOnDestroy for something else (e.g. cleaning up something not observable-like) it is quite easy to forget to call super.ngOnDestroy() in MyComponent which automatically introduces a memory leak
    4. And the last and the worst: unnecessary extends might be a bad architecture because a class can extend only once, in other words this code class MyComponent extends DestroyableComponent, SomethingElse is invalid and one needs to really pick what to extend. Example: let's say I have a library that provides me some class I am forced to extend. That means here I need to choose between the library and DestroyableComponent => it is clear that I will choose the library (because normally the library does the heavy stuff and DestroyableComponent is just a syntax sugar), which means in the end of the day that I would need to manually unsubscribe from everything in those kind of components.

    The proposed pipeable solves all the issues above. It just does not have any of them.

  4. bcandullo commented on Mar 28, 2018

    @bcandullo

    I agree, especially about #3. It seems the operator is elegant and most effective when presented with the alternatives, I’d love to see it implemented.

  5. wulfsberg commented on Mar 28, 2018

    @wulfsberg

    I was half on my way to propose a standard Observable which fires when the component is destroyed, so we could .takeUntil that, but creating it as a pipe would be more elegant. Now, if we could then also get @Input as observables using this pipe... :-)
    We definitely need a more elegant way to handle Observables on destroy, and this sounds like a good idea for it.

  6. ericmartinezr commented on Mar 29, 2018

    @ericmartinezr
    Contributor

    Have you seen this PR #20682 (See the issue referenced for more discussion on this matter)? Is it what you're asking for?

  7. dawidgarus commented on Mar 29, 2018

    @dawidgarus

    I'm not sure how new tree-shakeable providers will work, because documentation is lacking for now, but assuming that it would be possible make them provided at component level where they are injected, this could be easily done with something like this:

    @Component()
    export class MyComponent {
      constructor(takeUntilDestroyed: TakeUntilDestoyed) {
        interval(1000).pipe(takeUntilDestroyed.create).subscribe(console.log.bind(console));
      }
    }

    I think it's as easy as it gets, no hacks required and TakeUntilDestroyed can be implement without any changes in angular internals, something like:

    @Injectable()
    export class TakeUntilDestroyed implements OnDestroy {
      private destroy$: Subject<void> = new Subject<void>();
      create: <T>(o: Observable<T>) => Observable<T> = o => o.let(takeUntil(this.destroy$));
      ngOnDestroy(): void {
        this.destroy$.complete();
      }
    }
  8. smnbbrv commented on Mar 29, 2018

    @smnbbrv
    Author

    @ericmartinezr yes, it is a good solution. I would be happy if this pull request gets merged / released. Both solutions could co-exist together, still

  9. cedvdb commented on Mar 30, 2018

    @cedvdb

    I personally use this

    // utility class to unsub from observable
    export abstract class AutoUnsub implements OnDestroy {
    
    	_destroy$ = new Subject<void>();
    
    	constructor() { }
    
    	ngOnDestroy() {
    		this._destroy$.next();
    		this._destroy$.complete();
    	}
    }
    

    then in my component extending the class
    takeUntil(this._destroy$)

    Ultimately I think we should have a @Observable that'd do everything it the background.

  10. added
    featureLabel used to distinguish feature request from other issues
    on Apr 3, 2018
  11. modified the milestones: needsTriage, Backlog on Apr 3, 2018
  12. amcdnl commented on Apr 16, 2018

    @amcdnl
    Contributor

    This would be awesome to have implemented but I think in a more generic way the ability to listen in on lifecycle events in things like decorators would be a even better improvement. For example:

    export function MyDecorator() {
       return function(target: any, name: string, descriptor: TypedPropertyDescriptor<any>) {
          target.events.pipe(filter(event => event instanceof OnDestroy)).subscribe(() => {});
       }
    }
    

    Similar syntax to what the router uses with events and filtering techniques this would allow developers to listen for events in the component lifecycle and thus making decorators like mentioned here possible. A nice to have would be the ability to get the injector off the target without having to inject it.

    I realize this suggestion would be modifying the target properties in the component decorator (could be considered bad) and typescript might not be able to infer it correctly. Its just a suggestion to get us thinking about how we might accomplish this. On the flip side, you could just black list compilation of methods like ngOnDestroy out from AoT.

    Reference material on this subject:

    cc/ @robwormald

  13. NetanelBasal commented on Apr 16, 2018

    @NetanelBasal

    +1

  14. 1 remaining item

  15. MickL commented on Apr 1, 2019

    @MickL

    I would rather like to see this happen instead, which has 85 upvotes: #10185 then simply use takeUntil(this.destroy$) instead of writing a new rxjs operator.

    Until then, instead of using .unsubscribe() it is easier to use the operator takeUntil():

    @Component()
    export class PaymentMethodsLogosComponent implements OnInit, OnDestroy {
       destroy$ = new Subject();
    
       ngOnInit() {
          myObservable$
             .pipe(takeUntil(this.destroy$))
             .subscribe(() => {
                // ...
          });
       }
    
       ngOnDestroy() {
          this.destroy$.next();
          this.destroy$.complete();
       } 
    }
    
  16. frankiDotNet commented on Mar 6, 2020

    @frankiDotNet

    Back in 2020, is there still no way to auto unsubscribe if a component is destroyed?

  17. Bjeaurn commented on Mar 6, 2020

    @Bjeaurn
    Contributor

    Yes there is, if you use AsyncPipe. It's been there "since 2018." It also does not help this issue's discussion at all

  18. smnbbrv commented on Mar 6, 2020

    @smnbbrv
    Author

    How does async pipe help the discussion?

  19. frankiDotNet commented on Mar 6, 2020

    @frankiDotNet

    I was also thinking what AsyncPipe has to do with this?

  20. Bjeaurn commented on Mar 6, 2020

    @Bjeaurn
    Contributor

    auto unsubscribe if a component is destroyed?

    AsyncPipe does this for you. But unfortunately, still no lifecycle hooks provided by the framework we can hook into no.

  21. added
    feature: under considerationFeature request for which voting has completed and the request is now under consideration
    on Jun 4, 2021
  22. fabioemoutinho commented on Sep 19, 2022

    @fabioemoutinho
    Contributor

    @pkozlowski-opensource is it possible we introduce something like a @AutoUnsubscribe() myObs$ = ... that includes the observable into the logic view, and then when ngOnDestroy is called, it looks at the subscriptions added in logic view and unsubscribes them? I was thinking of something similar that was done for @Output()

  23. JeanMeche commented on Mar 16, 2023

    @JeanMeche
    Member

    This PR will land a takeUntilDestroyed operator !

  24. JeanMeche commented on Feb 19, 2024

    @JeanMeche
    Member

    The takeUntilDestroyed operator is available since v16.

  25. angular-automatic-lock-bot commented on Mar 21, 2024

    @angular-automatic-lock-bot

    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.

  26. locked and limited conversation to collaborators on Mar 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimecore: lifecycle hookscross-cutting: observablesfeatureLabel used to distinguish feature request from other issuesfeature: under considerationFeature request for which voting has completed and the request is now under consideration

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions