🤏

【React】useEffectの不要な依存値を取り除く方法

に公開

はじめに

ReactでuseEffectを使っていて、ESLintに「依存関係を追加してください」と怒られた経験はありませんか?
その通りにすると、意図しないタイミングでエフェクトが再実行されてしまい、悩んだことがある方も多いはずです。

この記事では、そんな不要な依存値を取り除く方法について、実践的な例と共に解説していきます。

基本的にリンタの指示通りに依存配列の中身を決める。

React公式ドキュメントで以下のような記述がありました。

エフェクトの依存配列は自分で「選ぶ」たぐいのものではないことに注意してください。エフェクトのコードで使用されるすべてのリアクティブな値は、依存値のリスト内で宣言されなければなりません。依存配列は、その周囲にあるコードによって決定されます。
依存配列は、エフェクトのコードで使用されるすべてのリアクティブな値のリストと考えることができます。

つまり、リンタのエラーの指摘が正しいので、基本依存配列は自分で考える必要はなく、リンタの指示通りにしてあげればよいのです。
正直私はこの記事を書くまで、自分で依存値を減らしているときが多々ありました。
なぜなら、依存配列にリンタが指摘したすべての変数を含めると、意図しないタイミングでエフェクトが再実行されてしまうことがあったからです。
ちなみに「リアクティブな値」とはReactの場合、再レンダリングごとに変わる可能性がある値で、代表的なのはpropsやstateなどです。

不要な依存値を取り除くには?

では、不要な依存値を取り除きたいとき、どのような対応をすればよいのでしょうか?
React公式ドキュメントで以下のような記述がありました。

依存配列を変えたいと思った時には、以下のフローを試します。
1.まず、エフェクトのコードやリアクティブな値の宣言部分を変更してみる。
2.次にリンタに指摘されたとおり、変更したコードに合わせるように依存配列を調整する。
3.その依存配列に満足できない場合は、最初のステップに戻る(コードを再度変更する)。

上記の「エフェクトのコードやリアクティブな値の宣言部分を変更してみる」ですが、不要な依存値を取り除く観点において、React公式では以下のような対応が書かれていました。

・コードをイベントハンドラに移動すべきでは?
・エフェクトが複数の互いに無関係なことを行っていないか?
・変更に「反応」せず値を読み出したいだけか?
・state の読み取りは次の state を計算するためか?
・リアクティブな値が意図せず変更されていないか?

今回は上記の中でも個人的に少しわかりづらいと思った「リアクティブな値が意図せず変更されていないか?」のみに焦点を当てて紹介します。

リアクティブな値が意図せず変更されていないか?

まず、「リアクティブな値が意図せず変更されていないか?」という問いの意味がわかりづらいので、まずは具体的なコードを示します。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

上記のコードのoptionsオブジェクトはリアクティブな値なので、useEffectの依存配列に含めています。
ここまではいいですが、実際にユーザーがメッセージを打つと、メッセージを打つたびにエフェクトが実行されてしまいます。
それはなぜか?原理としてはこうです。
メッセージが入力されるたびにmessageの状態が変わるので、コンポーネントが再レンダリングされる。
→コンポーネントが再レンダリングされたからoptionsオブジェクトを再生成する。optionsオブジェクトの中身は全く同じものでも再生成されたから前と違うものとして認識する。
(JavaScriptでは、新しく作成されたオブジェクトや関数は、他のすべてのオブジェクトや関数とは異なると見なされます。中身が同じであっても関係ない)
→依存値であるoptionsオブジェクトが変わったので、エフェクトが意図せずに実行される
これがリアクティブな値が意図せず変更されていないか?という問いの意味です。

ではどうすればよいか?

対策1

以下のように、optionsオブジェクトをエフェクトの外に出してしまうことで、依存配列を空にすることができます。

const options = {
  serverUrl: 'https://localhost:1234',
  roomId: 'music'
};

function ChatRoom() {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ All dependencies declared
  // ...

今回の場合は、optionsの中身が静的だったので、上記の対応でも問題ありません。
しかし、もしoptionsオブジェクト内のどちらかだけが静的だとすればどうでしょうか?
二つ目の対応策を見ていきます。

対策2

以下のようにエフェクト内にオブジェクトを移動させて、依存配列にオブジェクトではなく、roomIDという変数のみを入れます。

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

ここで出てくる疑問として、
1.なぜoptionsオブジェクトが依存配列ではなくなったのか?
2.なぜroomIdoptionsオブジェクトみたいに再生成されても同じものとして扱ってくれるのか?

まず1.に関してですが、そもそも依存配列に含めるべき変数は、useEffectの外部から渡される「リアクティブな値」です。
今回の場合、optionsオブジェクトはそもそもエフェクト内(ローカル)で宣言されているので、依存配列に含める必要はありません。
また今回の場合、serverUrlはコンポーネント外で静的な値として宣言しており、エフェクト内で唯一のリアクティブな値がroomIdとなっています。
次に2.に関してですが、roomIdはオブジェクトや関数ではないため、意図せず変わってしまうことはありません。JavaScriptでは、数値や文字列は内容によって比較されます。
そのため、依存配列にroomIdのみを含めた場合では、メッセージかかわるたびに再生成されたとしてもエフェクトが再び走ることはないのです。

最後に

今回は不要な依存値を取り除く方法として一つしか紹介しませんでしたが、他にもたくさんあります。
最終手段として、slint-ignore-next-line react-hooks/exhaustive-depsを記述してリンタを黙らせたり、リンタのエラーを無視するといった回避方法もありますが、基本的にはこれらの対処法は行うべきでなく、React公式ドキュメントでも「依存配列がコードと一致しない場合、バグが発生するリスクが非常に高くなります。」と記述されています。

React公式ドキュメントの最後のまとめを貼って今回の記事は終わりとしたいと思います。

まとめ
依存配列は常にコードと一致する必要がある。
依存配列が気に入らない場合、編集する必要があるのはコードの方である。
リンタを抑制すると非常にわかりにくいバグが発生するため、いかなる場合も避けるべきである。
依存値を削除するには、リンタにそれが不要であることを「証明」する必要がある。
特定のユーザ操作に応答してコードを実行する必要がある場合は、そのコードをイベントハンドラに移動する。
エフェクトの異なる部分が異なる理由で再実行される必要がある場合は、複数のエフェクトに分割する。
前のstateに基づいてstateを更新したい場合は、更新用関数を渡す。
最新の値を読み取りたいがそれに「反応」したくない場合、エフェクトからエフェクトイベントを抽出する。
JavaScriptでは、オブジェクトや関数は、異なるタイミングで作成された場合、異なる値だと見なされる。
オブジェクト型や関数型の依存値はなるべく避けるようにする。コンポーネントの外側かエフェクトの内側に移動させる。

Discussion