📌

JavaScriptのthisとReactでのイベントハンドラのバインド

2023/07/14に公開

ある企業のコーディングテストで、JavaScriptのクラスメソッドに関する問題が出題されて、そのときにthisに関して改めて調べてみると学びがあったのでメモを残しておきます。

import reportWebVitals from "./reportWebVitals";
import * as ReactDOM from "react-dom";
import { Component, ClassAttributes } from "react";

interface CounterProps extends ClassAttributes<Counter> {
  initialCount: number;
}
class Counter extends Component<CounterProps, any> {
  constructor(props: CounterProps) {
    super(props);
    this.state = {
      count: props.initialCount,
    };
  }
  handleAddClick() {
    this.setState({ count: this.state.count + 1 });
  }
  handleDownClick() {
    this.setState({ count: this.state.count - 1 });
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        <h1>{count}</h1>
        <div>
          <button onClick={this.handleAddClick}>up</button>
          <button onClick={this.handleDownClick}>down</button>
        </div>
      </div>
    );
  }
}
ReactDOM.render(
  <Counter initialCount={0} />,
  document.getElementById("root")
);

突然ですが上記のコードをみて、どこが問題なのかわかりますか?
この問題が分かる人はこの記事は読まなくても大丈夫です。

イベントハンドラが動かない理由

今回イベントハンドラが動かない理由はthisにあります。
先に結論を言うと、thisの値がundefinedになってしまっているのです。
JavaScriptは通常の関数を宣言すると内部にthisが設定されます。
これにより、関数の呼び出し方によってその関数の内部でthisは呼び出し元のオブジェクトに設定されます。
しかし、Reactはイベントハンドラを呼び出すときにthisをコンポーネントインスタンスに設定しないためthisの値がundefinedになってしまっているのです。
この問題を解決するための一つのが、手動でイベントハンドラをコンポーネントインスタンスにバインドすることです。

constructor(props: CounterProps) {
  super(props);
  this.state = {
    count: props.initialCount,
  };
  this.handleAddClick = this.handleAddClick.bind(this);
  this.handleDownClick = this.handleDownClick.bind(this);
}

このようにコンストラクタでイベントハンドラをバインドすることで、thisの値をコンポーネントインスタンスに設定することができます。

アロー関数におけるthisの挙動

実はもう一つの解決方法があります。
それはアロー関数を使うことです。
アロー関数は内部にthisを持ちません。
アロー関数では通常の関数宣言と違い関数が呼び出された時ではなく、関数がどこで定義されたかによってthisが設定されます。
そのため、関数定義をアロー関数に変更することでthisの値をコンポーネントインスタンスに設定することができます。

```ts 
handleAddClick() {
  this.setState({ count: this.state.count + 1 });
}
handleDownClick() {
  this.setState({ count: this.state.count - 1 });
}

JavaScriptのthisの挙動が複雑な理由

JavaScriptのthisの挙動が複雑な理由は、JavaScriptがプロトタイプベースの言語だからです。
プロトタイプベースのプログラミング言語とは、プロトタイプ(すでに存在するオブジェクト)を複製して新しいオブジェクトを作成するプログラミング言語です。
JavaScriptでは新しいオブジェクトはプロトタイプのプロパティや、メソッドを継承します。
さらに、新しいオブジェクトはプロトタイプのプロパティやメソッドを上書きすることができます。

let animal = {
  eats: true,
  walk() {
    console.log("Animal walk");
  },
};

let rabbit = Object.create(animal);  // 'animal' object is the prototype of 'rabbit'

rabbit.walk();  // outputs "Animal walk"

このコードではanimalオブジェクトをプロトタイプとしてrabbitオブジェクトを作成しています。
その結果、rabbitオブジェクトはanimalオブジェクトのwalkメソッドを継承しており、それを呼び出すことができます。

プロトタイプベース言語は柔軟性が高く、動的にオブジェクトを作成することができます。
しかし、一方でプロトタイプベース言語はその柔軟性ゆえにコードが予測しにくい原因にもなります。

thisの挙動を調べることで、JavaScriptの言語仕様を1つ理解することができました。

GitHubで編集を提案

Discussion