Skip to content

Instantly share code, notes, and snippets.

@btroncone
Last active April 19, 2022 22:29
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save btroncone/fe9d9aaf457f2ebd72c4624e34d664f8 to your computer and use it in GitHub Desktop.
Save btroncone/fe9d9aaf457f2ebd72c4624e34d664f8 to your computer and use it in GitHub Desktop.
Cleaning up subscriptions in Angular

Which do you prefer?

Adding to common sub, calling subscription.unsubscribe() in ngOnDestroy:

export class MyComponent {
  private _subscription: Subscription;
  
  ngOnInit() {
    this._subscription = myObservable
      .mergeMap(something)
      .subscribe(
        // some extra logic here
      );
      
    const anotherSubscription = myOtherObservable
      .mergeMap(somethingElse)
      .subscribe(
        // some extra logic here
      );
      
    this._subscription.add(anotherSubscription);
  }
  
  ngOnDestroy() {
    this._subscription.unsubscribe();
  }
}

Using takeUntil and private Subject, calling next in ngOnDestroy:

export class MyComponent {
  private _onDestroy = new Subject();
  
  ngOnInit() {
    myObservable
      .mergeMap(something)
      .takeUntil(this._onDestroy)
      .subscribe(
        // some extra logic here
      );
      
    myOtherObservable
      .mergeMap(somethingElse)
      .takeUntil(this._onDestroy)
      .subscribe(
        // some extra logic here
      );
  }
  
  ngOnDestroy() {
    this._onDestroy.next();
  }
}

*Note: takeUntil will also complete the observable before unsubscribing (thanks @GerardSans)

@SanderElias
Copy link

SanderElias commented Dec 13, 2017

I would prefer:

export class MyComponent {
  private _onDestroy = new Subject();
  myObservable =  someObservable
      .mergeMap(something)
      .takeUntil(this._onDestroy)
  myOtherObservable = someOtherObservable
      .mergeMap(somethingElse)
      .takeUntil(this._onDestroy)

  ngOnInit() { 
     this.myObservable.subscribe(...)
     this.myOtherObservable.subscribe(...)
  }
  
  ngOnDestroy() {
    this._onDestroy.next();
  }
}

And use async in the template. I left the subscribe in there because sometimes you need to do something in the code, but often that's not needed at all.
Aside from that, I try to combine all my observables so that I don't need multiple subscribes at all. Just one should be able to cut it. If it doesn't I might need to take a look at my abstractions and add some more service/components.

@btroncone
Copy link
Author

btroncone commented Dec 13, 2017

Cool approach with props and ngOnInit, thanks for sharing!

To your point, ideally subscribe within the component isn't needed, this example was a bit contrived to try to illustrate the point. I'm going to update with a placeholder in subscribe to make it a little more clear that some extra logic would be performed within that block.

@sandangel
Copy link

I would prefer:

export class MyComponent {
  private _subscriptions: Subscription[] = [];
  
  ngOnInit() {
    this._subscription.push(
     myObservable
      .mergeMap(something)
      .subscribe(
        // some extra logic here
      ),
     myOtherObservable
      .mergeMap(somethingElse)
      .subscribe(
        // some extra logic here
      )
   );
  }
  
  ngOnDestroy() {
    this._subscriptions.forEach(sub => sub.unsubscribe())
  }
}

@amikitevich
Copy link

We can use decorator to manage subscriptions in component:

function compose(...fns) {
  return function(...args) {
    return fns.reduceRight((acc, i) => i.apply(this, args), args);
  } 
}

export function SubCollector() {
  return function(target: any, key: string) {
    const subsMap = new Map();

    function unsubscribe() {
      this[key].forEach(sub => sub.unsubscribe());
      subsMap.delete(this);
    }

    Object.defineProperty(target, key, {
      configurable: false,
      get: function() {
        const subs = subsMap.get(this);
        if (!subs) {
          subsMap.set(this, []);
        }
        return subsMap.get(this);
      },
      set: function(newSub) {
        this[key].push(newSub);
      }
    });

    const old = target['ngOnDestroy'] || (() => null);
    Object.defineProperty(target, 'ngOnDestroy', {
      configurable: false,
      get: function() {
        return compose(unsubscribe, old).bind(this);
      }
    });
  };
}

@Component({
  selector: 'hello',
  template: `
    <h1>Hello {{name}}!</h1>
  `
})
export class HelloComponent {
  @Input() name: string;

  @SubCollector() sub;
  private time = Date.now();

  constructor(private _at: AtService) {}

  ngOnInit() {
    this.sub = Observable.interval(1000).subscribe(
      _ => console.log(this.time)
    );

  }
}

All subscriptions are added to array of subscriptions and implicitly unsubscribed when ngOnDestroy is called.

@btroncone
Copy link
Author

@sandangel I'm not sure I'm seeing the benefit of that over the first option, it's just introducing an array rather than using an already created subscription.

@amikitevich Really cool, thanks for sharing!

@hollygood
Copy link

i prefer the "takeUntil and private Subject, calling next in ngOnDestroy" option. But create a Decorator looks clean and easy, maybe a better option. Thanks @amikitevich

@kievsash
Copy link

kievsash commented May 3, 2019

+1 for takeUntil

@gurachan
Copy link

gurachan commented Nov 1, 2019

We can use decorator to manage subscriptions in component:

function compose(...fns) {
  return function(...args) {
    return fns.reduceRight((acc, i) => i.apply(this, args), args);
  } 
}

export function SubCollector() {
  return function(target: any, key: string) {
    const subsMap = new Map();

    function unsubscribe() {
      this[key].forEach(sub => sub.unsubscribe());
      subsMap.delete(this);
    }

    Object.defineProperty(target, key, {
      configurable: false,
      get: function() {
        const subs = subsMap.get(this);
        if (!subs) {
          subsMap.set(this, []);
        }
        return subsMap.get(this);
      },
      set: function(newSub) {
        this[key].push(newSub);
      }
    });

    const old = target['ngOnDestroy'] || (() => null);
    Object.defineProperty(target, 'ngOnDestroy', {
      configurable: false,
      get: function() {
        return compose(unsubscribe, old).bind(this);
      }
    });
  };
}

@Component({
  selector: 'hello',
  template: `
    <h1>Hello {{name}}!</h1>
  `
})
export class HelloComponent {
  @Input() name: string;

  @SubCollector() sub;
  private time = Date.now();

  constructor(private _at: AtService) {}

  ngOnInit() {
    this.sub = Observable.interval(1000).subscribe(
      _ => console.log(this.time)
    );

  }
}

All subscriptions are added to array of subscriptions and implicitly unsubscribed when ngOnDestroy is called.

do i need to have OnDestroy implemented to use it?

@AndreiShostik
Copy link

takeUntil() +

import { OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';

export interface UnsubscribeNotifier {
  unsubscribe$: Subject<void>;
}

export class BaseClass implements OnDestroy, UnsubscribeNotifier {
  public unsubscribe$ = new Subject<void>();

  public ngOnDestroy() {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}

and no need for over-complicated code

@junaidahmed501
Copy link

We can use decorator to manage subscriptions in component:

function compose(...fns) {
  return function(...args) {
    return fns.reduceRight((acc, i) => i.apply(this, args), args);
  } 
}

export function SubCollector() {
  return function(target: any, key: string) {
    const subsMap = new Map();

    function unsubscribe() {
      this[key].forEach(sub => sub.unsubscribe());
      subsMap.delete(this);
    }

    Object.defineProperty(target, key, {
      configurable: false,
      get: function() {
        const subs = subsMap.get(this);
        if (!subs) {
          subsMap.set(this, []);
        }
        return subsMap.get(this);
      },
      set: function(newSub) {
        this[key].push(newSub);
      }
    });

    const old = target['ngOnDestroy'] || (() => null);
    Object.defineProperty(target, 'ngOnDestroy', {
      configurable: false,
      get: function() {
        return compose(unsubscribe, old).bind(this);
      }
    });
  };
}

@Component({
  selector: 'hello',
  template: `
    <h1>Hello {{name}}!</h1>
  `
})
export class HelloComponent {
  @Input() name: string;

  @SubCollector() sub;
  private time = Date.now();

  constructor(private _at: AtService) {}

  ngOnInit() {
    this.sub = Observable.interval(1000).subscribe(
      _ => console.log(this.time)
    );

  }
}

All subscriptions are added to array of subscriptions and implicitly unsubscribed when ngOnDestroy is called.

Nice stuff man. cool 👍

@safalpillai
Copy link

@Totati
Copy link

Totati commented Nov 15, 2020

How about subsink? https://github.com/wardbell/subsink

We used to use it, but we went back using takeUntil

@safalpillai
Copy link

We used to use it, but we went back using takeUntil

Any particular reason or advantage for opting for takeUntil over Subsink?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment