概要
Reactコンポーネント内でコールバック関数を使用する際はアロー関数を使わないとエラーが起こります。
class App extends Component {
constructor() {
super();
this.state = {
inputText: ""
};
}
// 例のため、stateを変更するだけの関数
handleTextChange(e) {
const inputText = e.target.value;
this.setState({
inputText: inputText
});
}
render() {
return (
<input type="text"
onChange={(e) => this.handleTextChange(e)}
// onChange={this.handleTextChange} これだとエラーが起こる
/>
);
}
}
この理由についてthisがバンドルされないからと説明しているサイトは多いのですが、なぜバンドルされないのかがよく分からなかったので、自分で調べてみました。
React公式によると、これはReact特有の仕組みではないようです。
これは React に限った動作ではなく、JavaScript における関数の仕組みの一部です。
イベント処理 – React
以下その詳細です。
JavaScriptのthis
本題を解決する前提条件として、JavaScriptのthisについて理解しておく必要があります。
ただこれに関してはQiitaにも素晴らしい記事がたくさんあるので、そちらを見てもらったほうが早いです。
※参考
JavaScript の this を理解する多分一番分かりやすい説明 - Qiita
JavaScriptの「this」は「4種類」?? - Qiita
念のため最低限必要な知識についてまとめました。(コードは上記記事の引用です)
メソッド呼び出し
メソッド(オブジェクトのプロパティに代入された関数)呼び出しのときは、thisはそのメソッドを持つオブジェクトを指します。
var myObject = {
show: function() {
console.log(this); // thisはmyObject
}
}
myObject.show();
関数呼び出し
関数呼び出しのときは、thisはグローバルオブジェクトを指します。
ただしstrictモードのときはundefinedになります。
function show() {
'use strict'
console.log(this); // strictモードなのでthisはundefined
}
show();
var myObject = {
show: function() {
console.log(this); // thisはグローバルオブジェクト(Webブラウザ上だとwindowオブジェクト)を指す
}
}
var func = myObject.show;
func(); // 関数呼び出し
コンストラクタ呼び出し
thisは生成されたインスタンスを指します。
function MyObject() {
console.log(this) // thisはMyObjectインスタンスを指す
}
var myObject = new MyObject();
Reactコンポーネント内のthis
ここで本題。
先に示した例には3つのthisがあります。
class App extends Component {
constructor() {
super();
this.state = { // (1)コンストラクタ内のthis
inputText: ""
};
}
handleTextChange(e) {
const inputText = e.target.value;
this.setState({ // (2)関数内のthis
inputText: inputText
});
}
render() {
return (
<input type="text"
onChange={(e) => this.handleTextChange(e)} // (3)コールバック関数内のthis
/>
);
}
}
これらthisが指すものについて考えました。
(1)コンストラクタ内のthis
コンストラクタはReactDOMによってClass Componentがインスタンス化される際に実行されます。
上記例のコンストラクタ呼び出しに当たるので、thisはAppインスタンスを指します。
class App extends Component {
constructor() {
super();
this.state = { // このthisはAppインスタンスのことを指す
inputText: ""
};
}
// 〜省略〜
(3)コールバック関数内のthis
説明の都合から(3)コールバック関数内のthisを先に。
クラス内で定義されたメソッドをクラス内で実行するためには、呼び出しの際にthisをつける必要があります。
これはクラス内で定義されたメソッドは元をたどればプロトタイプのメソッドであるので、メソッドのある場所を示す必要があるからです。
つまりこの場合のthisもAppを指します。
※参考
JavaScriptのclass - Qiita
JavaScriptのプロトタイプからオブジェクト指向を学ぶ - Qiita
class App extends Component {
// 〜省略〜
render() {
return (
<input type="text"
onChange={(e) => this.handleTextChange(e)} // このthisもAppを指す
/>
);
}
}
(2)関数内のthis
(ⅰ)コールバック関数が通常の関数の場合
アロー関数を使わない場合、このthisはundefinedになります。
class App extends Component {
// 〜省略〜
handleTextChange(e) {
const inputText = e.target.value;
this.setState({ // thisはundefinedになる
inputText: inputText
});
}
render() {
return (
<input type="text"
onChange={this.handleTextChange} // アロー関数を使わない
/>
);
}
}
これはonChangeが実行されるときに、ただの関数呼び出しとなるためです。(onChange()のような形になる)
クラス構文は自動的にstrictモードで実行されるため、thisはグローバルオブジェクトではなくundefinedとなります。
ここでReact公式の説明を振り返ると、
これは React に限った動作ではなく、JavaScript における関数の仕組みの一部です。
イベント処理 – React
ようするに、以下と同じ動きが起こっているようです。
var myObject = {
'use strict'
show: function() {
console.log(this); // thisはundefined
}
}
var func = myObject.show;
func(); // 関数呼び出し
(ⅱ)コールバック関数がアロー関数の場合
アロー関数はthisを束縛しません。
つまりアロー関数で呼ばれた関数内のthisは、thisを囲っている外でのthisを指します。
MDN Web Docsの言葉を引用すると、
スコープに this 値がない場合、その一つ外側のスコープで this 値を探します
アロー関数 - JavaScript | MDN
よってこの場合のthisはAppを指します。
class App extends Component {
// 〜省略〜
handleTextChange(e) {
const inputText = e.target.value;
// アロー関数で呼び出しているため、thisは束縛されない。
// handleTextChange関数スコープにthisの定義はないため、このthisは一つ外側のスコープであるAppを指す
this.setState({
inputText: inputText
});
}
render() {
return (
<input type="text"
onChange={(e) => this.handleTextChange(e)} // アロー関数を使用
/>
);
}
}
結論
Reactコンポーネント内で定義したメソッドをコールバック関数で呼び出した場合は、通常はただの関数呼び出しとなるため、呼び出し先の関数内のthisはundefindedとなる。
アロー関数で呼び出した場合はthisが束縛されず、一つ外のスコープであるクラスコンポーネント(インスタンス)を指すようになる。
bindについて
thisをバインドする方法としてアロー関数を使うやり方の他に、bindメソッドを使うこともできます。
class App extends Component {
constructor() {
super();
this.state = {
inputText: ""
};
// 呼び出された関数内のthisがAppを指すように、bindメソッドを用いてAppにバインドさせておく
this.handleTextChange = this.handleTextChange.bind(this); // この行のthisは3つともAppを指す
}
handleTextChange(e) {
const inputText = e.target.value;
this.setState({ // 関数呼び出しの際にthisがAppにバインドされているため、thisはAppを指す
inputText: inputText
});
}
render() {
return (
<input type="text"
onChange={this.handleTextChange} // 通常の関数
/>
);
}
}
実はコールバック関数にアロー関数を使用する場合の問題点として、render()メソッドが呼ばれるたびに異なるコールバック関数が毎回作成されてしまうということがあります。
これはReactの差分レンダリングと相性が悪く、コールバックがpropsの一部として下層のコンポーネントに渡される場合、それら下層コンポーネントが余分に再描画されてしまいます。
bindメソッドはその問題の対策にもなるようです。
(一部公式より引用)
2021年1月追記
React v17現在、React.memoとuseCallbackを使用することで関数の再作成を防ぐことができます。
参考
・React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする - Qiita
・雰囲気で使わない React hooks の useCallback/useMemo - Qiita
参考まとめ
JavaScript の this を理解する多分一番分かりやすい説明 - Qiita
JavaScriptの「this」は「4種類」?? - Qiita
JavaScriptのclass - Qiita
JavaScriptのプロトタイプからオブジェクト指向を学ぶ - Qiita
イベント処理 – React
アロー関数 - JavaScript | MDN
クラス - JavaScript | MDN
This is why we need to bind event handlers in Class Components in React