Never Bind in Render

With React + ES6/7 usage in full swing, I’ve seen a lot of examples and code floating around that bind functions to this in their render methods.

Like these:

<MyComponent onClick={() => this.onClick(id)} />
<MyComponent onClick={this.onClick.bind(this, id)} />
<MyComponent onClick={::this.onClick(id)} />

Don’t do that!

By binding your event handlers like this, you create brand new functions every time React calls your component’s render.

toaster ejecting bread onto floor

Ok, that’s fine if it’s just some TodoMVC implementation or toy project - but in the real world you’re probably trying to improve performance with pure rendering and making use of things like Redux, maybe Immutable.js, etc, to help you.

All of that goes out the window if you bind functions in every component that has an event handler. This is especially bad for performance because those are usually the components that are used hundreds of times in your interface such as table rows and the like.

Here’s some pseudo-app stuff:

class TodoList extends React.Component {
  onToggleComplete( id ) {
    dispatch( { type: 'TODO_TOGGLE_COMPLETE', id } )
  }

  render() {
    return (
      <ul>
        {
          this.props.todos.map( todo =>
            <li key={todo.id}
                onClick={() => this.onToggleComplete(todo.id)}>
              {todo.complete ? '✓' : '–'} {todo.text}
            </li>
          )
        }
      </ul>
    )
  }
}

Then maybe you render this app like so:

function render() {
  ReactDOM.render(
    <TodoList todos={store.getState().todos} />,
    document.getElementById('root')
  )
}

store.subscribe( render )
render()

So you click the todo and it dispatches your action, the store updates and we re-render to let React do its magic.

What you’re thinking will happen is that on re-render only the single todo item you changed will actually re-render, because that’s the only object that has changed.

To be clear, if you toggle the 1st of 2 todos, you expect:

  • store.getState().todos -> new object
  • store.getState().todos[0] -> new object
  • store.getState().todos[1] -> same object as before

And you’d be right. But what happens? Every todo item is re-rendered anyway. One of the props to the li component is a new object - the result of binding the onToggleComplete function - and so the component will be re-rendered. That means the ‘new’ event handler will be attached to the DOM, and the old handler garbage collected (both slow).

On top of that, in this example, it’s just a single li with a couple text nodes. But it will be an even bigger problem if you’re rendering a Ticket component (or 10, or 100) that might have a dozen possibly complex children, and giving it a bound-in-render handler.

What I Do Now?

Fortunately there’s an easy fix for the binding problem:

@connect()
class TodoList extends React.Component {
  // You don't have to use this syntax.
  // You could bind it in your constructor,
  // or use someting like https://github.com/andreypopp/autobind-decorator
  onToggleComplete = ( id ) => {
    dispatch( { type: 'TODO_TOGGLE_COMPLETE', id } )
  }
  // ...
}

But wait, where does the id of the todo come from? We’re back to square one and would need the original binding code again :(

What we have done here is expose a code smell. We shouldn’t be using a bare li. Instead it should be a Todo component that has the toggle functionality built in and can know the correct todo to operate on.

class TodoList extends React.Component {
  render() {
    return (
      <ul>
        {
          this.props.todos.map( todo =>
            <Todo key={todo.id} todo={todo} />
          )
        }
      </ul>
    )
  }
}

class Todo extends React.Component {
  onToggleComplete = () => {
    dispatch( {
      type: 'TODO_TOGGLE_COMPLETE',
      id: this.props.todo.id
    } )
  }
  render() {
    const todo = this.props.todo
    return (
      <li key={todo.id}
          onClick={this.onToggleComplete}>
        {todo.complete ? '✓' : '–'} {todo.text}
      </li>
    )
  }
}

Ah… Your render is pure again.