Skip to content

Instantly share code, notes, and snippets.

@fongandrew
Last active January 14, 2024 16:02
  • Star 54 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save fongandrew/f28245920a41788e084d77877e65f22f to your computer and use it in GitHub Desktop.
Explaining why we bind things in React

Start With This

Before getting to React, it's helpful to know what this does generally in Javascript. Take the following snippet of code. It's written in ES6 but the principles for this predate ES6.

class Dog {
  constructor() {
    this.favoriteWord = "Woof!";
  }
  
  bark() {
    return this.favoriteWord;
  }
}

let dog = new Dog();
dog.bark(); // => Woof!

Cool. That makes sense. But remember, functions are also objects in their own right in Javascript, so let's try this:

let bark = dog.bark;
bark(); // => Error

Ruh roh. What happened? You probably got a message saying favoriteWord wasn't a property of undefined, or something similar. But this.favoriteWord clearly refers to the Dog instance, so what gives?

The answer is that this is determined at the time the function is called, not the time the function is defined. This can be super confusing if you're coming from, say, Python, where the following code works:

class Dog:
  def __init__(self):
    self.favorite_word = "Woof!"
  
  def bark(self):
    return self.favorite_word;

dog = Dog()
bark = dog.bark
bark() # => Woof!

It's tempting to say this and self do the same things. They both refer to the parent of a function in this case. But in the case of Python, self is determined at the time the function is defined. In the case of Javascript, this is determined at the time the function is called.

The bark variable is distinct from dog.bark. To make this a little clearer, let's just use a different name.

let bark2 = dog.bark;
bark2();

We're creating a new variable (bark2). bark2 doesn't have a parent at the time it's called, unlike dog.bark (which has dog as a parent). bark2 is a top level variable, so it's "parent" is undefined. undefined doesn't have a favoriteWord property, so calling the function results in an exception.

Before a Javascript expert shouts at me, note that in Javascript-land, we generally think of the relationship between dog and bark not as "parent" and "child", but that dog is the "context" for bark. dog.bark means "Call the bark function with the context of dog". And this always refers to the context in a given function.

To understand why someone (if not necessarily you or me) might think this is cool, consider the following:

let cat = {
  favoriteWord: "Meow!"
};
cat.meow = bark;
cat.meow(); // => "Meow!"
bark();     // Nope, still broken

This works because the this variable in the bark function isn't tied to the original dog object. So we can freely assign bark to cat.meow. And when we call cat.meow, the caller is cat, so this.favoriteWord refers to "Meow!" instead of "Woof!".

As an aside, the "context" for a Javascript function or class method is distinct from "context" in React. As the React developers themselves indicate, if you're just getting started with React, ignore React's version of context. However, you do need to understand context in the Javascript sense insofar that you're using classes to represent React components and invoking stuff like this.props or this.setState({ ... }).

Bind

OK, let's add a new wrinkle. Consider this now:

let alwaysWoof = bark.bind(dog);
alwaysWoof(); // => "Woof!"

Why does this work? It's because calling bind on a function returns a copy of that function in which this is always set to whatever arg you pass to bind. This applies even if we change the caller of the bound function:

cat.meow = alwaysWoof;
cat.meow(); // => "Woof!"

In the class context, it's pretty common to bind to this:

class ConsistentDog {
  constructor() {
    this.favoriteWord = "Woof!";
    
    let bark = function() {
      return this.favoriteWord;
    }
    this.bark = bark.bind(this);
  }
}

let conDog = new ConsistentDog();
let conBark = conDog.bark;
conBark(); // => "Woof!"

Writing .bind(this) over and over is pretty annoying, so in ES6, you can also avoid writing .bind(this) with the () => ... syntax:

class SimpleDog {
  constructor() {
    this.favoriteWord = "Woof!";
    this.bark = () => this.favoriteWord;
    
    /*
      Or you can do this if you need more than one statement 
      for your function.
      
      this.bark = () => {
        let simpleWord = this.simpleWord;
        return simpleWord;
      };
    */
  }
}

let simDog = new SimpleDog();
let simBark = simDog.bark;
simBark(); // => "Woof!"

Now with React Classes

Still with us? OK, now to bring in React. Consider this React component, defined as an ES6 class:

class Welcome extends React.Component {
  render() {
    return <button onClick={this.sayName}>Say My Name</button>;
  }
  
  sayName() {
    alert(this.props.name);
  }
}

In React, you invoke like this: <Welcome name="Bob" />. This renders a button. Clicking the button should trigger an alert with "Bob".

Except it doesn't. Because in the above example, this would be undefined in the sayName function.

What's happening inside the render function is that this refers to the current instance of our React component. That component has a sayName function defined, so this.sayName points to our function, just fine and dandy.

But what React is doing behind the scenes is assigning this.sayName to another variable. That is, it's just like this:

let onClick = this.sayName;
onClick(); // Technically a click event is passed to onClick
           // but this doesn't matter for our purposes

And just like our dog example, we get an error. Because this is undefined. This is extra confusing because in previous versions of React, React would "autobind" the event handler for you, so it would work. But at some point, Facebook decided to stop doing that, so ... here we are.

So how can we fix our component? We just do binding ourselves, like this:

<button onClick={this.sayName.bind(this)}>Say My Name</button>;

Or with ES6 syntax:

<button onClick={() => this.sayName()}>Say My Name</button>;

And it should work!

One final note -- when we bind a function in React, we can do that not only when the render function is called, but before as well. So take this:

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.boundSayName = this.sayName.bind(this);
  }

  render() {
    return <button onClick={this.boundSayName}>Say My Name</button>;
  }
  
  sayName() {
    alert(this.props.name);
  }
}

We can do this.boundSayName instead of this.boundSayName.bind(this). Because this.boundSayName was already bound to this in the constructor.

And that's it! Hope it helps!

@dungle-scrubs
Copy link

Really helpful, thanks so much!

@azena-t
Copy link

azena-t commented Jan 14, 2024

Well, isn't that a barrel of laughs! Thank you for that article, that's really helped explain it to me, particularly around the idea of performing an action within a given context rather than the typical parent child relationship in other languages.

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