23

I have a component which gets its data from subscribing to a store.

this.store.select('somedata').subscribe((state: any) => {
  this.somedata = state.data;
});

I want to unsubscribe from this subscription when component is no more, in other places where I am subscribing to some observable, something like this:

this.service.data.subscribe(
   (result: any) => {//data}
);

I unsubscribed it on ngOnOnDestroy, like this:

ngOnDestroy(){
   this.service.data.unsubscribe();
}

But in case of store I'm not able to, it gives me error:

Property 'unsubscribe' does not exist on type 'Store<State>'
Uzair Khan
  • 2,812
  • 7
  • 30
  • 48

6 Answers6

39

There's a better way than the top voted answer, a way in which you don't have to manage a bunch of subscriptions, only one. Then you can have as many subscriptions as you want without having to create a bunch of unnecessary vars.

  public ngDestroyed$ = new Subject();

  public ngOnDestroy() {
    this.ngDestroyed$.next();
  }

  public ngOnInit() {
    this.myWhateverObs$
        .pipe(takeUntil(this.ngDestroyed$))
        .subscribe((val)=> { this.whatever()});
    this.myWhateverNPlusOne$
        .pipe(takeUntil(this.ngDestroyed$))
        .subscribe((val)=> { this.whatever()})
  }
FlavorScape
  • 13,301
  • 12
  • 75
  • 117
  • 5
    thanks for the solution. i have multiple subscriptions in app. and using the above method is a nightmare. – Raj Oct 21 '18 at 16:53
  • This should be the proper answer, it's a breeze to use with multiple subscriptions. – Bogdan Constantinescu Dec 06 '18 at 16:44
  • 1
    In my case, I had to use next() instead of complete() at ngOnDestroy – Téwa Dec 10 '18 at 13:43
  • 1
    calling `this.ngDestroyed$.complete()` would not cause takeUntil to complete subscription. you have to call `.next()` instead. Then you don't have any need for calling complete and unsubscribe. – Dmitriy Snitko Mar 13 '19 at 13:23
  • If you'd want to add a `switchMap` or other, would you add it before or after the `takeUntil()`? –  Dec 09 '19 at 20:25
  • @altu usually takeUntil would be last – FlavorScape Jan 29 '20 at 17:30
  • 'create a bunch of unnecessary vars.' this is not correct. The subscriptions are created whenever the .subscribe method is called, so there are no extra vars created in the top voted solution. If you don't want to keep track of a variable per subscription, you can create a subscriptions: Subscription[] object. – Jacopo Lanzoni Feb 17 '20 at 09:14
  • @JacopoLanzoni I would call having to create a variable per subscription or array of them quite unnecessary. – FlavorScape Mar 24 '20 at 20:48
  • 2
    One must call `ngDestroyed$.complete()` after `ngDestroyed$.next()`, otherwise you will leak the subject. It might be a small object but it will remind active... About order: `takeUntil` is always last, with the expectation of `shareReply`, `multicast` and similar multicasting operators. – Akxe Dec 23 '20 at 11:13
32

When you subscribe you will receive a subscription object on it you can call unsubscribe()

const subscription = this.store.select('somedata').subscribe((state: any) => {
  this.somedata = state.data;
});
// later
subscription.unsubscribe();

or

ngOnInit(){
 this.someDataSubscription = this.store.select('somedata').subscribe((state: any) => {
  this.somedata = state.data;
 });
}

ngOnDestroy(){
  this.someDataSubscription.unsubscribe();
}
G.Vitelli
  • 1,229
  • 9
  • 18
  • 3
    There's an easier and less messy way to do this for many subscriptions. See the answer with `takeUntil(this.$ngDestroyed)` – FlavorScape Nov 05 '18 at 21:05
8

You can get value without directly calling subscribe method, get value by async pipe like

@Component({
    template: `
        <div>Current Count: {{ counter | async }}</div>
    `
})
class MyAppComponent {
    counter: Observable<number>;

    constructor(private store: Store<AppState>){
        this.counter = store.select('counter');
    }
}

Here we are using async pipe for getting value. The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.

Akshay Garg
  • 1,010
  • 8
  • 15
  • What if you need to use the 'store observable' inside of your .ts file to get a value from a store slice? You would need to use subscribe in that case, no? – Mark Oct 04 '18 at 16:49
  • @Mark you can use `map`, `filter`, `switchMap` and many other `rxjs`'s operators to modify your store as much as you wish, without any need to subscribe. You can then pass your modified `Observable` in the template with `async` pipe, as @Akshay described. – matewka Jun 17 '19 at 20:27
5

The neatest way I've used is using ngx-take-until-destroy library. Your code will be something like this:

this.store.select('somedata')
    .pipe(untilDestroyed(this))
    .subscribe((state: any) => {
        this.somedata = state.data;
    });

You also need to have ngOnDestroy() method in your class.

Joe Mayo
  • 7,501
  • 7
  • 41
  • 60
mahyar
  • 49
  • 1
  • 2
4

You don't need to subscribe in first place use | async in your template. Everything you get from store is observable, let angular handle your subscription. Here is api

stojevskimilan
  • 970
  • 8
  • 12
  • 1
    What if you need to use the 'store observable' inside of your .ts file to get a value from a store slice? You would need to use subscribe in that case, no? – Mark Oct 04 '18 at 16:49
2

This answer is based on the answers provided by FlavorScape and by mahyar.

No external libraries solution

One way to avoid bloating each component with the subject and its code is to use a base component (tested with Angular 10.0.6):

base.component.ts

import { Subject } from "rxjs";
import { Component } from "@angular/core";

@Component({
    selector: "app-base-component",
    template: ""
})
export class BaseComponent {
    public ngDestroyed$ = new Subject();

    public onDestroy(): void {
        this.ngDestroyed$.next();
    }
}

foo.component.ts

@Component({
    selector: "app-foo",
    templateUrl: "./foo.component.html",
    styleUrls: ["./foo.component.scss"]
})
export class FooComponent extends BaseComponent implements OnInit, OnDestroy {

    fooList$: Observable<FooModel[]>;

    @ViewChild(DataBindingDirective) dataBinding: DataBindingDirective;
    public gridData: any[];
    public gridView: any[];

    public mySelection: string[] = [];

    constructor(private readonly store: Store<AppState>) {
        super();
    }
    ngOnDestroy(): void {
        this.onDestroy();
    }

    ngOnInit(): void {
        this.store.dispatch(ApplicationFooItemsRequestedAction());
        this.fooList$ = this.store.select(selectAllApplicationFooItems);

        this.fooList$.pipe(takeUntil(this.ngDestroyed$)).subscribe(ul => {
            // do stuff with items
        });
    }
}

Using an external library

You can use @ngneat/until-destroy library to avoid custom code and also support other scenarios (e.g. within services)

@Component({
    selector: "app-foo",
    templateUrl: "./foo.component.html",
    styleUrls: ["./foo.component.scss"]
})
export class FooComponent extends BaseComponent implements OnInit, OnDestroy {

    ngOnInit(): void {
        this.store.dispatch(ApplicationFooItemsRequestedAction());
        this.fooList$ = this.store.select(selectAllApplicationFooItems);

        this.fooList$.pipe(takeUntil(untilDestroyed(this))).subscribe(ul => {
             // do stuff with items
        });
     }

}
Alexei - check Codidact
  • 22,016
  • 16
  • 145
  • 164
  • 1
    You forgot a very key aspect! You did not complete the `ngDestroyed$` subject itself. It will be kept in memory in this way... – Akxe Dec 23 '20 at 11:07