Skip to content

Instantly share code, notes, and snippets.

@btroncone
Last active April 19, 2022 22:29
Show Gist options
  • 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)

@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