Events Pub/Sub How to unsubscribe in lifecycle hook

I’m subscribing to a 'user:loggedin' event I am publishing from elsewhere in my app and I’m not able to figure out how to unsubscribe on onPageWillLeave

here’s a shortened version of my page:

import {Page, NavController, NavParams, Events} from 'ionic-angular';

@Page({
  templateUrl: 'build/pages/listings_detail/listings_detail.html'
})
export class ListingsDetailPage {

  constructor(
    private nav: NavController,
    private params: NavParams,
    private events: Events
  ) {

  }

  loginHandler(user) {
    console.log('called loginhandler', user);
  }

  ngOnInit() {
    this.events.subscribe('user:loggedin', (user) => {
      this.loginHandler(user);
    });
  }

  onPageWillLeave() {
    // doesn't work
    this.events.unsubscribe('user:loggedin', this.loginHandler);
    // also doesn't work
    // this.events.unsubscribe('user:loggedin', (user) => {
    //   this.loginHandler(user);
    // });
  }
}

I am able to subscribe to this just fine, however I’m unable to unsubscribe. If I take a look at the events map after entering and leaving this page multiple times, I can see that the events are just piling up.

I’ve tried a ton of variations of this and haven’t found anything that works. The only way I was able to unsubscribe was to pass in a null handler when unsubscribing.

this.events.unsubscribe('user:loggedin', null);

However, this cancels all subscriptions that other components are listening in on which is unacceptable.

Anyone have unsubscribe working on a page in a lifecycle hook? Any help is MUCH appreciated.

4 Likes

this.loginHandler isn’t what you’re passing to subscribe: you’re passing an anonymous fat arrow function. So either actually do pass this.loginHandler or stash the fat arrow function somewhere to use as the second argument to unsubscribe.

@rapropos Thanks for the feedback, and I am using handlers in other places that work fine (i.e. inside the contructor in app.js) however when I do use a similar tactic such as:

  loginHandler(user) {
    this.loggedIn(); // gives error
  }

  ngOnInit() {
    this.events.subscribe('user:loggedin', this.loginHandler);
  }

  onPageWillLeave() {
    this.events.unsubscribe('user:loggedin', this.loginHandler);
  }

  loggedIn() {
    console.log('this is never called');
  }

this is the original way I tried it but I’m getting the following error (which is why I went the fat arrow route)

EXCEPTION: TypeError: Cannot read property 'loggedIn' of undefined

meaning this has become undefined

I tried the fat arrow route after looking at the sample ionic2 conference app https://github.com/driftyco/ionic-conference-app/blob/f229cfae822c63fc24489f2e4ee392500ee3e5f4/app/app.js

1 Like

on a side note, using stashed handlers does indeed cancel the subscription… however since this is undefined, it doesn’t solve the problem for me.

This doesn’t make sense to me. What code are you trying? I was thinking of something like this:

private _loginsub: (user:User) => void;

ngOnInit():void {
  this._loginsub = (user) => {
    this.loginHandler(user);
  };
  this.events.subscribe('user:loggedin', this._loginsub);
}

onPageWillLeave():void {
  if (this._loginsub) {
    this.events.unsubscribe('user:loggedin', this._loginsub);
    this._loginsub = undefined;
  }
}

EDIT: fixed a couple of syntax errors caught by @inki.

16 Likes

This solution works for me, thanks! :smiley:

So, just to make sure I understand this correctly…

  1. You are creating the handler using a fat arrow function inside of ngOnInit
  2. You are setting it to this._loginsub
  3. And passing it to subscribe as the handler

So, because this.loginHandler(user) is wrapped in a fat arrow function:

this._loginsub = (user) => {
  this.loginHandler(user);
});

so that this is bound properly, correct?

Instead of me incorrectly trying to pass in this.loginHandler which is just a reference to the function and has no bindings to this

Just thinking out loud and hopefully helping other (and me) for future reference.

Yes, but that’s not required. It could be done in the constructor, if you wish. All that’s required is that this be properly assigned to your ListingDetailsPage object at the time the function is defined. Internally what the fat arrow is doing is defining a lexically scoped variable _this, assigning it to what this is at function definition time, and then using it in place of this inside the function. If you’re familiar with the notion of closures in languages like Lisp, it’s like that.

I would phrase this the other way around: we’re setting this._loginsub to that function, which is the true “handler” from Ionic’s perspective.

The key is to ensure you pass exactly to unsubscribe what you gave to subscribe. The reason your second commented out “also doesn’t work” attempt didn’t work is because two separately-defined anonymous fat arrow functions aren’t equal, even if they’re internally intended to do the same thing. If you go to a bodega and buy a candy bar and then come back the next day to the exact same store and buy the exact same brand of candy bar, you have two different candy bars.

4 Likes

rapropos thanks for your insight here. You’re responses to this thread actually helped me with a different problem I was facing with events.

I was trying to subscribe to the same event in two places and unsubscribe from one when some promise resolved. It seems that Ionic Event unsubscribe is pretty greedy when it comes to anonymous handlers and it was removing that event from the topics array for both events. By passing in a function created this way into handler, unsubscribe now recognizes which handler I want to unsubscribe from and it works flawlessly.

Thanks man!

1 Like

Hi there, I was also struggling with this and your post was a great help. Just as an enhancement I think that if you just do the below it’ll also work in the same way but it’ll be less verbose:

this.loginHandler = (user) => {
      // .......... your code
};
1 Like

I think it should be ionViewWillLeave in place of onPageWillLeave

Also I changed the ngOnInit to ionViewWillEnter

Ionic 2 NavController docs

but other than that your solution works super good !! cheers

2 Likes

The API for those lifecycle events changed a few months after that comment.

Thanks for the awesome discussion about events… that helped a lot!

I’m having issue with Tabs, where a tab has a subscribe cant unsubscribe cause we have some issues on ionic . but use ngOnDestroy to subscribe that works https://github.com/driftyco/ionic/issues/8547#issuecomment-275265611

1 Like

your solution does work in a normal situation where simple pages are navigating but it fails on a tabbed page design. when I leave the main tabs root page the ion view did leave events for all the sub tabs are never fired. I had to work around and make all my tab pages subscribe to the ionViewWillLeave() event on my tab root page

There is an open issue about that situation.

Thanks for the solution, however it’s still crazy that they don’t return a wrapped structure that provides a way to unsubscribe like a normal observable.

Thanks. It works on my side

I’m having a similiar issue so I don’t want to start another thread. Ionic 3.
Everything works. I make my event. Do what it does. I Leave the view, it stops. But when I enter again it makes the eventlistener 2 times, and so on.

//Declaring (you know where this is, don't want to type out all the export class MapPage etc)
private otherUserHandler: Function; 

ionViewWillEnter(): void {
    this.omitHandlers(); // Assigns a function to this.otherUserHandler, writing it below. Also tried making this in 
                                    // the constructor.
    if(this.variables.singleUserOnMap === true){
        this.events.subscribe('updateOtherUsersLocation', this.otherUserHandler) //Here I subscribe
      }
    }else{
      
    }
  }

//And when I leave the page
ionViewWillLeave(): void {
    this.events.unsubscribe('updateOtherUsersLocation', this.otherUserHandler); //Here I unsubscribe
    this.otherUserHandler = undefined;

  }

//Function to assign what this.otherUserHandler should be
omitHandlers(): void {
    this.otherUserHandler = (lat, lon, floor) => {
      console.log('inside eventlistener') // I see these logs become more and more, that's how I know it's called 
                                                            // several times
      this.updateOtherUserMarker(lat, lon)
    }
  }

Just want to say that I found my issue, I am doing it correct in the code above. But when I enter the page again it’s publishing data again inside a new Observable. So it’s the publish function I need to handle.