【React】ビルドインフックの依存配列を書くときに気を付けること
はじめに
useEffect
等の依存配列を書いていた時に、eslint
でreact-hooks/exhaustive-deps
の意図しない警告が出たので原因をまとめてみました。
事象
警告がでたのは以下ようにstate
オブジェクト内のメソッドを依存関係配列に追加したときです。
const Example = () => {
const [state, setState] = useState({
func: () => {
return;
},
param: 'content',
});
useEffect(() => {
state.func();
}, [state.func]);
return <div>{state.param}</div>
}
eslint-plugin-react-hooks
が入っている環境であれば、useEffect
内の依存配列で、
React Hook useEffect has a missing dependency: 'test'. Either include it or remove the dependency array.
という警告がでます。
依存配列を[state]
とすると警告が消えました。
useEffect
コールバック関数内ではstate.func
しか使用していないにも関わらず、[state.func]
とするとダメなんですね。
疑問に思ったので少し調べてみました。
前提知識
今回の現象を理解するにはまずJavaScriptのthis
を理解する必要があります。
JavaScript上でthis
は、
- 実行コンテキスト
- コンストラクタ
- 関数とメソッド
- Arrow Function
のいずれかの条件下によってふるまいを変えます。
今回は関数とメソッドにフォーカスします。
関数・メソッド内のthis
次のコードを見てください。
function func() {
console.log(this);
}
func();
コードを実行すると、ブラウザであればWindow
オブジェクトが出力されます。
なぜなら、関数の場合はthis
に実行コンテキストが暗黙的に代入されるからです。
ブラウザの実行コンテキストはWindow
なので、Window
がthis
として出力されたわけですね。
では、次のコードではどうでしょうか。
function func() {
console.log(this);
}
const baseObj = {
func,
param: 'param',
};
baseObj.func();
実行結果は、
{
"func": function func() {\n console.log(this);\n},
"param": "param"
}
となります。
つまり、メソッド(オブジェクトのプロパティとして定義されている関数)は定義されているオブジェクトそのもの(ベースオブジェクト)をthis
に代入していることが分かります。
補足(bindについて)
ちなみに、this
に代入されるオブジェクトを変更(バインディング)したい場合はbind
メソッドを使う必要があります。
function func() {
console.log(this);
}
const baseObj = {
func: func.bind(this),
param: 'param',
};
baseObj.func();
baseObj
を定義するときのthis
の値(実行コンテキスト)をメソッドbaseObj.func
のthis
として代入することができます。
よって、実行するとWindow
オブジェクトが出力されます。
結論
さて、以上を踏まえてもう一度今回起きた事象を考えてみましょう。
少し極端な例ですが、Example
コンポーネントを以下のように実装したとします。
function func() {
console.log(this.counter);
}
const Example = () => {
const [state, setState] = useState({
func,
counter: 0,
});
const increment = () => {
setState(current => ({
...state,
counter: current.counter + 1,
}));
};
const output = useCallback(() => {
state.func();
}, [state.func]);
return (
<div>
<span>counter: {state.counter}</span>
<button onClick={increment}>increment</button>
<button onClick={output}>output</button>
</div>
);
};
このとき、incrementボタン
をクリックするとカウンターがインクリメントされ画面の表示はcounter: 1
となります。
しかし、outputボタン
をクリックしてカウンターの値を出力しても0
となります。
理由は次の通りです。
-
関数increment
内でstate
をシャローコピーしたものをセットしている -
関数increment
が実行されても、state.func
への参照は変化しない -
関数output
はuseCallback
でメモ化されており、依存配列のstate.fucn
が変化しない限り更新されない
これは、メソッドがベースオブジェクトに暗黙的に依存しているときに起こりえます。
今回の例では、メソッドfunc
がベースオブジェクトのプロパティcounter
に依存しています。
eslint-plugin-react-hooks
では、このようにメソッドがベースオブジェクトに依存している可能性を考慮し警告を出していることが分かりました。
逆に言えば、eslint-plugin-react-hooks
を採用していないプロジェクトでは、メソッドがベースオブジェクトに依存していないか十分に考慮する必要があります。
だだし、eslint-plugin-react-hooks
はメソッドがベースオブジェクトに依存していない場合も警告が出てしまいます。
特にuseEffect
など、state
が変化した場合でも副作用を起こさせたくないときもあると思います。
その場合は、eslintの警告を行単位で無効にするeslint-disable-next-line react-hooks/exhaustive-deps
を使用するか、(個人的にはeslint-disable-next-line
を使用したくないので)分割代入を行うと警告が表示されなくなります。
const Example = () => {
const [state, setState] = useState({
func: () => {
return;
},
param: 'param',
});
const { func } = state;
useEffect(() => {
func();
}, [func]);
return <div>{state.param}</div>
}
最後に
このようにJavaScriptにおいてメソッドは暗黙的にベースオブジェクトに依存する場合があるので、依存配列にメソッド書く際はそのメソッドがベースオブジェクトに依存していないか十分考慮しないと想定外のバグを生む可能性があるので注意しましょう。
基本はeslint-plugin-react-hooks
を使っていれば大丈夫だと思います。
Discussion