Porting several React projects to ES6, the lack of autobind is a serious annoyance. Previously you could rely on the compiler to automatically call autoBind on your event listeners allowing the following code to work as you’d expect:

1
2
3
4
5
6
7
8
9
10
class MyComponent = React.createClass({
  componentDidMount() {
    // Autobind will make this work
    MyFluxStore.addListener(this.onChange);
  }
  onChange(state) {
    // 'this' is actually 'this'. No need for .bind
    this.setState(state);
  }
}

This is no longer the case when you have migrated to ES6, here we need to call bind ourselves to make sure we get the proper behaviour.

1
2
3
4
5
6
7
8
9
10
class MyComponent extends React.Component {
  componentDidMount() {
    // the event listener needs an explicit bind
    MyFluxStore.addListener(this.onChange.bind(this));
  }
  onChange(state) {
    // 'this' is what we expect
    this.setState(state);
  }
}

The problem comes once the component in unmounted. Because bind always returns a new function, simply calling MyFluxStore.removeListener will not work because this.onChange.bind(this) != this.onChange.bind(this). The event handler will remain on the store, leaking the listener and memory.

The solution here is to store away the event handler in the constructor as a member variable, and use that when binding / unbinding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyComponent extends React.Component {
  constructor() {
    super();

    this._onChange = this.onChange.bind(this);

    this.state = { ... };
  }
  
  componentDidMount() {
    // the event listener needs an explicit bind
    MyFluxStore.addListener(this.onChange);
  }
  
  componentWillUnmount() {
    // Will remove the correct listener
    MyFluxStore.removeListener(this._onChange);
  }

  onChange(state) { ... }
}

This leads to a lot of cruft code though. Every bind/unbind cycle needs to be listed thrice in every component. Which is a drag. To solve this, I made an extension to the BaseStore class from flux. This changes the addChangeListener and overrides EventEmitter.removeListener in order to allow you to simple pass the same arguments and it will find the listener you need to remove. Allowing you to rewrite the code above as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyComponent extends React.Component {
  constructor() {
    super();

    this.state = { ... };
  }
  
  componentDidMount() {
    // Pass the arguments you want to bind after the function, store will handle binding
    MyFluxStore.addListener(this.onChange, this);
  }
  
  componentWillUnmount() {
    // Pass same arguments like in componentDidMount and it will find the function and remove it
    MyFluxStore.removeListener(this.onChange, this);
  }

  onChange(state) { ... }
}

How does this magic work? It simple keeps a list in the store of what arguments were passed with a reference to the bound function. Then when you remove a listener it searches the internal list and remove that listener.

This carries no performance overhead when the listener is called, only a small loss when a listener is removed (because of the need to search for it in the list).

The code is available as a Github gist, feel free to comment there if you have remarks or comments.

Below is the excerpted addChangeListener function. As can be seen, it keeps a reference to the bound function by adding it to the global _listenerArguments map, and adds an internal tag to the listener. If you look at removeChangeListener in the gist above you will see it uses the bound listener to reverse the operation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  addChangeListener(listener) {
    if (typeof listener != 'function') {
      throw new TypeError("'listener' argument must be a function");
    }

    if (arguments.length == 1) {
      // Simple callback, just register it
      this.on(Constants.CHANGE_EVENT, listener);
    }
    else {
      // More complex, bind it, and keep a reference
      let extraArguments = Array.prototype.slice.call(arguments, 1);
      let boundListener = listener.bind.apply(listener, extraArguments);

      // Remember the arguments we bound with
      boundListener._listenerId = _listenerIds++;
      _listenerArguments[boundListener._listenerId] = extraArguments;

      // Register the listener
      this.on(Constants.CHANGE_EVENT, boundListener);
    }
  },