Losing this binding in class methods

So now we've made all of our Products, we've gotten through the big sale and now we're revising the site's user interface.

For each of the products, we want users to be able to favorite them, and to click this heart button in slide and have it stored in the account to view later and maybe purchase. But we're revising the functionality, so that users can only do that if they are signed in, otherwise, we tell them they need to need to sign in first.

So once again we have a Product class, but with now we have some user data too. For each product, we have the name and price properties. And we have this handleFavoriteProduct method, which checks to see if the user is authenticated, by looking at this isAuth boolean. If so, the product will be favorited. We'll add this product to this array of their favorites and alert them that it was successful.

const isAuth = true;
const user = {
  favorites: [],
};

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  handleFavoriteProduct() {
    if (isAuth) {
      this.favoriteProduct();
    } else {
      console.log("You must be signed in to favorite products!");
    }
  }

  favoriteProduct() {
    user.favorites.push(this.name);
    console.log(`${this.name} favorited!`);
  }
}

So by default, if we create a simple product, say the coasters that we saw, and the user is authenticated, we should be able to run handleFavoriteProduct, and we get the alert that this product was in fact favorited.

const product1 = new Product("Coasters", 89.99);
product1.handleFavoriteProduct();

Let's say we are running this method in response to a user click, which we'll see how to do very soon. And we can see that this alert shows up very quickly. Maybe we want to delay that a second. So to do that, we pass the favorite product method to a setTimeout, which accepts favorite product as a callback, and we'll tell it to be executed after 1 second or 1000ms instead.

class Product {
...
  handleFavoriteProduct() {
    if (isAuth) {
      setTimeout(this.favoriteProduct, 1000);
    } else {
      console.log("You must be signed in to favorite products!");
    }
  }
}

Now when we run handle favorite product, we just see favorited, we don't see the products name.

product1.handleFavoriteProduct(); // ' favorited!'

So the value of this doesn't refer to the class anymore. It's being lost. How can we fix this?

We've seen in the past how functions create their own context, which can change what this refers to, causing unexpected results. And again, since classes are merely functions and can also include functions in the form of methods, we have to be aware of problems with binding this correctly in relation to classes, too.

When we used setTimeout, which accepted favoriteProduct as a function, it changed what this was bound to from the Product class. We will use a number of similar functions which accept callback functions and implicitly change the this context of that function. Try to think of functions that we've already encountered that accept callbacks other than setTimeout. Many of the ones that come to mind for me are the array methods, such as .map, .filter and reduce. Here's just a quick reminder of the syntax to refresh your memory:

function callback() {
  // do something with array
}

[].map(callback);

Think back to the lesson about arrow functions and methods, remember what was special about arrow functions that helped us in referring to the proper this context? It is the fact that arrow functions don't create a new this binding. It refers to the this binding one level above. So in this case, if we were to write this.favoriteProduct as an inline arrow function instead of passing it as a reference to the setTimeout, what would the this context now be?

class Product {
...
  handleFavoriteProduct() {
    if (isAuth) {
      setTimeout(() => this.favoriteProduct(), 1000);
    } else {
      console.log("You must be signed in to favorite products!");
    }
  }
}

It would refer one level up from the setTimeout, which is the class once again. So if we run handleFavoriteProduct again:

product1.handleFavoriteProduct(); // 'Coasters favorited!'

We get the right result! this is now bound correctly to the class and we can use its instance properties, like name.

Note that another way to leverage arrow functions to fix the this problem is to make favoriteProduct a property in this constructor, which is equal to an arrow function. This might look strange, but the principle still applies and it stiill works correctly:

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
    this.favoriteProduct = () => {
      user.favorites.push(this.name);
      console.log(`${this.name} favorited!`);
    };
  }

  handleFavoriteProduct() {
    if (isAuth) {
      setTimeout(this.favoriteProduct, 1000);
    } else {
      console.log("You must be signed in to favorite products!");
    }
  }

  // favoriteProduct() {
  //   user.favorites.push(this.name);
  //   console.log(`${this.name} favorited!`);
  // }
}

Arrow functions are a simple fix and will be able to solve your this binding problems with classes the majority of the time, but we don't want to have to stuff all our methods into our constructor as properties.

An arguably better and more expressive solution to our problem is to explicitly set the this context that the function refers to (in this case and in most cases, the class), through the .bind() method.

It says exactly what it does: it binds the this keyword that the function uses to whatever we need it to be. So for our favoriteProduct method, it needs to always be bound to the class itself to be able to get it's name instance property. So how do so with the .bind() method?

We can do so by, where we're executing favoriteProduct in setTimeout, chain on .bind() after the function and pass in 'this'. This may seem a bit unclear at first, saying that 'this' should be bound to 'this', but it's just saying, the favoriteProduct method that we're getting from the class, should always refer to the class and nothing else.

After you attach .bind() to a function, it is, for intents and purposes, bound to the provided this context forever, whenever the function is used, and cannot be lost:

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  handleFavoriteProduct() {
    if (isAuth) {
      setTimeout(this.favoriteProduct.bind(this), 1000);
    } else {
      console.log("You must be signed in to favorite products!");
    }
  }

  favoriteProduct() {
    user.favorites.push(this.name);
    console.log(`${this.name} favorited!`);
  }
}

So again, if we run this:

// 'Coasters favorited!'

We get the correct result.

bind works great and will always be bound correctly in that one method, handleFavoriteProduct. However, if we were to use it in another method, we would have to bind it again.

How do we fix having to bind a method multiple times? We can make all references to the method bound properly with bind by overwriting the method in the constructor

We can bind this method to the class and create a property of the same name in the constructor. This will make sure that every time we reference that method in our class, it is using the bound version of the function. And yes, as you've likely noticed, this is very similar to creating an arrow function in the constructor.

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;

    this.favoriteProduct = this.favoriteProduct.bind(this);
  }

  handleFavoriteProduct() {
    if (isAuth) {
      setTimeout(this.favoriteProduct, 1000);
    } else {
      console.log("You must be signed in to favorite products!");
    }
  }

  favoriteProduct() {
    user.favorites.push(this.name);
    console.log(`${this.name} favorited!`);
  }
}

The benefit of this approach is that we are leaving our methods where they should live, the class body. They’re merely bound in the constructor. So you define all your methods in the right place, the body. You declare your properties in the right place, the constructor. And you set your context in the same place, the constructor, as well.

Review

In review, I would recommend using the .bind() approach, even though all the approaches we covered are valid. It is the most explicit, even though it what we're doing in the constructor may seem strange at first.

If you still don't feel confident about this, don't worry. We're going to do a deep dive into this and how to know what it refers to in a later video as well as how to explicitly set what it refers to. There are rules as to how this keyword operates and without knowing these rules, it value seems mysterious because, as this lesson demonstrated, it can be silently set to something else without us knowing and this happens routinely to even the best developers.

Looking to the future

As a final note, you might be wondering why we can't just declare all of our methods directly on the class body as arrow functions to avoid any problems with this.

In the future, it is very likely that we will have a revised syntax so that we can rewrite our Product class as follows, with methods as arrow functions and where we don't need a constructor at all.

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  handleFavoriteProduct = () => {
    if (isAuth) {
      setTimeout(this.favoriteProduct, 1000);
    } else {
      console.log("You must be signed in to favorite products!");
    }
  };

  favoriteProduct = () => {
    user.favorites.push(this.name);
    console.log(`${this.name} favorited!`);
  };
}

This is called the class fields proposal and is something to look forward to in future versions of ECMAScript when it becomes the standard.

For now, if you’re encountering unexpected behaviors or errors when using working with this, try explicitly binding the appropriate context. Other than that, I don't tend to give this much thought as it relates to classes