React.jsのrenderの戻り値の中で.bindで新しい関数を定義してはいけないわけ

Reactのコードサンプルなどを見ると、下に引用したようなコードをよく見かけます。ES6のクラス式に慣れてない方のために解説すると、ここでは渡されたpropsvalue値とhandleChange関数を、Inputタグのコールバックに渡すさいにbind関数を使って、render関数内で動的に新しい関数を定義しています。

// 悪い例:render関数内で新しい関数を定義
class MyComponent extends React.Component {
  render() {
    const { value, handleChange } = this.props;
    return <input value={value} onChange={handleChange.bind(this)} />
  }
}

この例の他にも、onChange={(ev) => handleChange(ev)}などのようにアロー関数を使う例もよく見るかもしれません。こちらはアロー関数式で作られた新しい関数が、呼び出し元のスコープに束縛される特性を使って、thisを束縛しています。

この書き方自体に、シンタックス上の問題があるわけではなく、またReactもエラーを出さずに動作します。では何が問題なのでしょうか。

描画パフォーマンスへのペナルティ

実はこのようにrender関数内で関数を動的に作ってしまうと、reactがコンポーネントをアップデート(再描画)するたびに、新しい関数が作られてしまいます。最初のペナルティは、この実行時における関数の定義と破棄に伴うオーバヘッドです。大きなページで大量のエレメントを同時にアップデートする際などに、無視できない影響が出ます。

もう一つの大きな影響は、Reactが再描画の判断をする際にLambdaやBindで作られた関数は実行するたびに新しいものが作られているので、前の描画結果との比較結果が必ずfalseになり、必要以上に再描画が実行されてしまうことになります。

このあたりの話は以下のブログ記事が参考になります。

👉 Component Rendering Speed in React

reactjs_component_rendering_performance_03.png

DOMの描画が不必要に多くなると、画面のスムースなアップデートが行われないなどの副作用が出やすいので避けるほうが無難でしょう。

修正例:constructor関数内で定義する

ES6のクラスを使っている場合は、constructor関数内であらかじめbindするか、クラスのプロパティとして定義してしまう方法が一般的でしょう。この場合は、関数の作成がインスタンス化時のみになるので、描画時のペナルティを避けることができます。

// 修正例1:コンストラクタ内でbind関数を使用して、Thisを束縛
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.props.handleChange.bind(this);
  }

  render() {
    const { value, handleChange } = this.props;
    return <input value={value} onChange={handleChange} />
  }
}
// 修正例2:クラス・プロパティとしてLambdaを使って関数を定義し、Thisを束縛
class MyComponent extends React.Component {

  handleChange = ev => this.props.handleChange(ev);

  render() {
    const { value } = this.props;
    return <input value={value} onChange={this.handleChange} />
  }
}

.map()を使って複数のコンポーネントを描画する場合はコンポーネントを切り出す

またよくある状況として配列内の要素を順番に描画するという際に、一つのコールバック関数に特定の引数を予め割り当てたいと言うような場合があります。

// 悪い例
class Items extends React.Component {
  render() {
    const { items, handleClick } = this.props;
    return (
      <ul>
        {items.map((item, idx) => <li key={idx} onClick={handleClick.bind(null, item.id)}
      </ul>
    )
  }
}

この場合は、パフォーマンスが問題になる場合は要素を別のコンポーネントにしてしまって、そちらでコールバックに引数を割り当てる方法があります。

// 修正例
class Item extends React.Component {
  handleClick() {
    const { item: { id }, onItemClick } = this.props;
    onItemClick(id);
  }

  render() {
    const { item: { name } } = this.props;
    return <li onClick={this.handleClick}>{name}</li>;
  }
}

class Items extends React.Component {
  render() {
    const { items, handleClick } = this.props;
    return (
      <ul>
        {items.map((item, idx) => <Item item={item} onItemClick={handleClick} />}
      </ul>
    )
  }
}

関数式を使ってrefを定義する場合

これは公式で推奨されている方法なので、例外として認めても良い気もするのですが、Lintingツールを使うとエラーで怒られるので対処法を書いておきます。

// 関数式のRef
class MyComponent extends React.Component {
    render() {
        return (<button ref={(el) => this.buttonElement = el}>Click Me!</button>);
    }
}

個人的によくやるのはClass内にRefをハンドルするための関数群をオブジェクトとして定義しておいて、そちらをアサインするやり方です

// Refハンドラ関数を外出(TypeScriptで書いてます)
class MyComponent extends React.Component<P, S> {
    private refHandlers: any = {
        button: (ref: HTMLButtonElement): void => { this.buttonElement = ref; },
    };
    render() {
        return (<button ref={this.refHandlers.button}>Click Me!</button>);
    }
}

参考:Lintingツールを使う

bindアロー関数式を使ったコールバック関数の定義は、非常に簡単に書けるので無意識にJSX内で使ってしまいがちです。避けるにはlintingツールを使うのが効果的でしょう。個人的にはES6で書くときはESLINTの、TypeScriptで書くときはTSLINTの以下のそれぞれのルールを有効にしています。

Update

2016/12/26 Fix Typo