potato face
CryingPotato

Who's Binding This?

#javascript

I got a PR that looked like this to review at work recently:

class A extends React.Component {
    // ...

    function b() {
        this.setState({ 'a': 1 }, this.afterUpdateA);
    }
    // ...
    b();
}

React’s setState (this app hasn’t been migrated fully to hooks yet) takes a callback that’ll be called after the state update has successfully happened. This is a poor-man’s useEffect, but does have the nice advantage of only executing when you want it to (instead of on every change like useEffect would).

If you’re not familiar with Javascripts “prototypes”, this article is a fantastic primer

Now, I was sure this snippet would break because of a classic this binding issue with this.afterUpdateA. Typically you’d have to wrap the callback in an arrow function or manually bind to this, but my coworker insisted that everything worked, and.. it did! I scratched my head for a while before hitting the old “LGTM” button on GitHub. Still, this bothered me the following weekend so I went down React-flavored rabbit hole to get to the bottom of this mystery.

First to sanity check my understanding of this-binding I cooked up a simple example:

Codeblocks like the one below are fully editable! Cannon is awesome.

Ok, now let’s try calling f from a setTimeout (to mimic the setState callback pattern):

As expected, this.val is undefined because this is not bound to the instance of A in the callback! Well, not nice, but we expected this (not the Javascript this). Let’s try to recreate a similar minimal React example with setState:

import React from 'react';

class Car extends React.Component {
  constructor() {
	super();
    this.state = { color: 'red' }
  }

  changeState() {
    this.setState({
      color: 'blue'
    }, this.logColor)
  }

  logColor() {
    console.log(`Logging color ${this.state.color}`)
  }


  render() {
    return (
      <>
        <h2>Hi, I am a {this.state.color} Car!</h2>
        <button onClick={() => this.changeState()}>Change me</button>
      </>
    )
  }
}

export default Car

The important function above is changeState, which is called every time you click on the button. If you did click changeState, you’d see that it correctly logged the color blue, which means.. this was bound, by someone! But how? Where in the React code was this magic happening?? I was bamboozled.

My first approach was to RTFM, but nowhere on React’s website did it mention that React was internally binding this on the setState callback. Devestated by the manual not having the information I needed (so rare), I turned to my old pals at StackOverflow. Skimming through a few setState callback answers, I discovered that the 10x posters on the orange website also didn’t realize that there was some implicit binding going on.

The wise internet having failed me in my quest for answers, it was time to take matters into my own hands by Reading the Code. Unfortunately, thanks to the transpilation and cross-compilation and minifying and whatever the hell else webpack does to my beautiful tsx, it was basically impossible to grok what was happening by just jumping to definitions through my editor. I did attempt to grep minified React for uses of bind, but hit a dead end there as well.

It was at this moment I decided to take the lazy approach (that I should have all along) by inserting a debugger; statement in my changeState function and pray to the Chrome PMs that they found it in their hearts to make the debugger magically work with React apps. After hitting the old :w on my editor and clicking the button on the page, I was seamlessly dropped into some bonkers React javascript. This was the first time I’d ever clicked through the internals of a React app (I’m an infra engineer by day), and I felt right at home with complexity that rivaled parsing Datadog docs to understand how much money they’d charge you. After setting breakpoints at random spots (the Chrome debugger is shockingly ergonomic), and accidentally stepping over functions I wanted to step into, I found the magical line:

function callCallback(callback, context) {
  if (typeof callback !== 'function') {
    throw new Error('Invalid argument passed as callback. Expected a function. Instead ' + ("received: " + callback));
  }
  callback.call(context);
}

callback.call passes in a context object, which is actually the React instance variable - ladies and gentlemen, we got him. Going through the React codebase on Github, sure enough, we see this undocumented binding. this was no easy mystery to solve.