💭

【React】ビルドインフックの依存配列を書くときに気を付けること

2023/12/21に公開

はじめに

useEffect等の依存配列を書いていた時に、eslintreact-hooks/exhaustive-depsの意図しない警告が出たので原因をまとめてみました。

事象

警告がでたのは以下ようにstateオブジェクト内のメソッドを依存関係配列に追加したときです。

index.jsx
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

次のコードを見てください。

index.js
function func() {
  console.log(this);
}

func();

コードを実行すると、ブラウザであればWindowオブジェクトが出力されます。
なぜなら、関数の場合はthisに実行コンテキストが暗黙的に代入されるからです。
ブラウザの実行コンテキストはWindowなので、Windowthisとして出力されたわけですね。

では、次のコードではどうでしょうか。

index.js
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メソッドを使う必要があります。

index.js
function func() {
  console.log(this);
}

const baseObj = {
  func: func.bind(this),
  param: 'param',
};

baseObj.func();

baseObjを定義するときのthisの値(実行コンテキスト)をメソッドbaseObj.functhisとして代入することができます。
よって、実行するとWindowオブジェクトが出力されます。

結論

さて、以上を踏まえてもう一度今回起きた事象を考えてみましょう。
少し極端な例ですが、Exampleコンポーネントを以下のように実装したとします。

index.jsx
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への参照は変化しない
  • 関数outputuseCallbackでメモ化されており、依存配列の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を使用したくないので)分割代入を行うと警告が表示されなくなります。

index.jsx
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