リアクティブエフェクトのライフサイクル

エフェクトは、コンポーネントとは異なるライフサイクルを持ちます。コンポーネントはマウント、更新、アンマウントを行うことができます。エフェクトは、何かを同期を開始し、後で同期を停止するという2つのことしかできません。エフェクトがプロップと状態に依存していて、それらが時間とともに変化する場合は、このサイクルが複数回発生する可能性があります。Reactは、エフェクトの依存関係を正しく指定しているかどうかを確認するためのリンタールールを提供しています。これにより、エフェクトは最新のプロップと状態に同期された状態を維持します。

学習内容

  • エフェクトのライフサイクルがコンポーネントのライフサイクルとどのように異なるか
  • 個々のエフェクトを独立してどのように考えるか
  • エフェクトが再同期する必要がある場合と、その理由
  • エフェクトの依存関係がどのように決定されるか
  • 値がリアクティブであるとはどういう意味か
  • 空の依存配列の意味
  • Reactがリンターを使用して依存関係が正しいことをどのように検証するか
  • リンターに同意しない場合の対処法

エフェクトのライフサイクル

すべてのReactコンポーネントは同じライフサイクルを経ます

  • コンポーネントは、画面に追加されるとマウントされます。
  • コンポーネントは、通常はインタラクションに応答して、新しいプロップまたは状態を受け取ると更新されます。
  • コンポーネントは、画面から削除されるとアンマウントされます。

これはコンポーネントを考える良い方法ですが、エフェクトについてはそうではありません。 代わりに、コンポーネントのライフサイクルとは無関係に、個々のエフェクトを独立して考えてみてください。エフェクトは、外部システムを現在のプロップと状態に同期する方法 を記述します。コードが変更されると、同期はより頻繁に、またはあまり頻繁に行われる必要があります。

この点を説明するために、コンポーネントをチャットサーバーに接続するこのエフェクトを考えてみましょう。

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

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

エフェクトの本体は、同期を開始する方法 を指定します。

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...

エフェクトによって返されるクリーンアップ関数は、同期を停止する方法 を指定します。

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...

直感的には、Reactはコンポーネントがマウントされると同期を開始し、コンポーネントがアンマウントされると同期を停止する と考えるかもしれません。しかし、これは物語の終わりではありません!場合によっては、コンポーネントがマウントされたままである間にも、複数回同期を開始および停止する 必要が生じる可能性があります。

これがなぜ必要なのか、いつ発生するのか、そしてどのようにこの動作を制御できるのかを見てみましょう。

注記

一部のエフェクトは、クリーンアップ関数をまったく返しません。ほとんどの場合、 返す方が良いでしょうが、返さない場合は、空のクリーンアップ関数を返したかのようにReactが動作します。

同期が複数回必要になる理由

ChatRoomコンポーネントが、ユーザーがドロップダウンで選択するroomIdプロップを受け取るとします。最初に、ユーザーがroomIdとして"general"ルームを選択します。アプリは"general"チャットルームを表示します。

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

function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

UIが表示された後、Reactは同期を開始するためにエフェクトを実行します。"general"ルームに接続します。

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...

今のところ順調です。

その後、ユーザーはドロップダウンで別のルーム(たとえば、"travel")を選択します。まず、ReactはUIを更新します。

function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

次に何が起こるべきかを考えてみましょう。ユーザーはUIで"travel"が選択されたチャットルームであることを確認します。しかし、前回実行されたEffectはまだ"general"ルームに接続されたままです。roomIdプロップが変更されたため、以前に行ったEffectの処理("general"ルームへの接続)はもはやUIと一致しません。

この時点で、Reactには2つのことを行ってもらいたいと考えています。

  1. 古いroomIdとの同期を停止する("general"ルームから切断する)。
  2. 新しいroomIdとの同期を開始する("travel"ルームに接続する)。

幸いなことに、すでにReactにこれらの両方の方法を教えました! Effectの本体は同期を開始する方法を指定し、クリーンアップ関数は同期を停止する方法を指定します。Reactが行う必要があるのは、それらを正しい順序で、正しいプロップと状態を使用して呼び出すだけです。それがどのように行われるかを見てみましょう。

ReactによるEffectの再同期方法

ご承知のとおり、ChatRoomコンポーネントは、roomIdプロップに新しい値を受け取りました。以前は"general"でしたが、現在は"travel"です。Reactは、異なるルームに再接続するために、Effectを再同期する必要があります。

同期を停止するために、Reactは"general"ルームに接続した後にEffectが返したクリーンアップ関数を呼び出します。roomId"general"だったため、クリーンアップ関数は"general"ルームから切断します。

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...

次に、Reactはこのレンダリング中に提供したEffectを実行します。今回はroomId"travel"であるため、同期を開始し"travel"チャットルームに接続します(そのクリーンアップ関数が最終的に呼び出されるまで)。

function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...

これのおかげで、ユーザーがUIで選択したのと同じルームに接続できるようになりました。危機回避です!

異なるroomIdでコンポーネントが再レンダリングされるたびに、Effectは再同期されます。たとえば、ユーザーがroomId"travel"から"music"に変更したとします。Reactは再び、そのクリーンアップ関数を呼び出すことによって("travel"ルームから切断することによって)、Effectの同期を停止します。次に、新しいroomIdプロップを使用して本体を実行することによって("music"ルームに接続することによって)、同期を再開します。

最後に、ユーザーが別の画面に移動すると、ChatRoomはアンマウントされます。これで、接続を維持する必要はまったくなくなります。ReactはEffectの同期を最後に停止し、"music"チャットルームから切断します。

Effectの観点からの考察

ChatRoomコンポーネントの観点から起こったすべてを要約しましょう。

  1. roomId"general"に設定された状態でChatRoomがマウントされました。
  2. roomId"travel"に設定された状態でChatRoomが更新されました。
  3. roomId"music"に設定された状態でChatRoomが更新されました。
  4. ChatRoomがアンマウントされました。

コンポーネントのライフサイクルのこれらの各時点で、Effectは異なる動作をしました。

  1. Effectは"general"ルームに接続しました。
  2. Effectは"general"ルームから切断し、"travel"ルームに接続しました。
  3. Effectは"travel"ルームから切断し、"music"ルームに接続しました。
  4. Effectは"music"ルームから切断しました。

では、Effect自身の観点から何が起こったかを考えてみましょう。

useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);

このコードの構造は、起こったことを重ならない時間の連続として見るきっかけになるかもしれません。

  1. Effectは"general"ルームに接続しました(切断されるまで)。
  2. Effectは"travel"ルームに接続しました(切断されるまで)。
  3. Effectは"music"ルームに接続しました(切断されるまで)。

以前は、コンポーネントの観点から考えていました。コンポーネントの観点から見ると、Effectを「コールバック」や「ライフサイクルイベント」として考え、「レンダリング後」や「アンマウント前」など特定の時間に発生するものでした。この考え方はすぐに複雑になるので、避けるのが最善です。

代わりに、常に一度に1つの開始/停止サイクルに焦点を当てましょう。コンポーネントがマウント、更新、またはアンマウントされているかどうかは問題ではありません。必要なのは、同期を開始する方法と停止する方法を記述することだけです。うまく行けば、Effectは必要な回数だけ開始および停止されても堅牢になります。

これは、JSXを作成するレンダリングロジックを作成するときに、コンポーネントがマウントされているか更新されているかを考えないことを思い出させるかもしれません。画面に表示されるものを記述すると、Reactは残りを処理します。

ReactによるEffectの再同期能力の検証方法

実際に操作できるライブ例を以下に示します。「チャットを開く」を押すと、ChatRoomコンポーネントがマウントされます。

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  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>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

コンポーネントが初めてマウントされると、3つのログが表示されることに注意してください。

  1. ✅ "general" ルームへの接続中 (https://localhost:1234…) (開発環境のみ)
  2. ❌ "general" ルームから切断されました (https://localhost:1234) (開発環境のみ)
  3. ✅ "general" ルームへの接続中 (https://localhost:1234…)

最初の2つのログは開発環境のみです。開発環境では、Reactは常に各コンポーネントを一度再マウントします。

Reactは、開発環境ではすぐに再同期を強制することで、Effectが再同期できることを検証します。これは、ドアのロックが機能するかを確認するために、ドアを開閉する動作に似ています。Reactは、実装されたクリーンアップが適切であることを確認するために、開発環境ではEffectを余分な回数開始および停止します。Effectが開発環境で2回実行される方法の対処法

実際には、Effectが再同期される主な理由は、Effectが使用するデータの一部が変更された場合です。上記のサンドボックスで、選択されたチャットルームを変更してみてください。roomIdが変更されると、Effectが再同期されることに注意してください。

しかし、再同期が必要となる、より特殊なケースもあります。たとえば、チャットが開いている間に、上記のサンドボックスでserverUrlを編集してみてください。Effectがコードの編集に応じて再同期されることに注意してください。将来、Reactは再同期に依存する機能をさらに追加する可能性があります。

ReactがEffectの再同期が必要であると認識する方法

roomIdの変更後に、ReactがEffectの再同期が必要であるとどのように認識したのか疑問に思われるかもしれません。それは、そのコードがroomIdに依存していることをReactに伝えたからです。依存関係のリストに含めることで。

function ChatRoom({ roomId }) { // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// ...

これがその仕組みです。

  1. roomIdはpropであり、時間とともに変化する可能性があることを知っていました。
  2. EffectがroomIdを読み取るため(したがって、そのロジックは後で変わる可能性のある値に依存している)、知っていました。
  3. そのため、Effectの依存関係として指定しました(roomIdが変更されたときに再同期されるように)。

コンポーネントが再レンダリングされるたびに、Reactは渡された依存関係の配列を確認します。配列内の値のいずれかが、前回のレンダリング時に渡された同じ位置の値と異なる場合、ReactはEffectを再同期します。

たとえば、最初のレンダリング時に["general"]を渡し、後のレンダリング時に["travel"]を渡した場合、Reactは"general""travel"を比較します。これらは異なる値です(Object.is と比較して)、そのため、ReactはEffectを再同期します。一方、コンポーネントが再レンダリングされてもroomIdが変更されていない場合、Effectは同じルームに接続されたままになります。

各Effectは、個別の同期プロセスを表します。

既に記述したEffectと同時に実行する必要があるという理由だけで、関連のないロジックをEffectに追加することは避けてください。たとえば、ユーザーがルームにアクセスしたときに分析イベントを送信したいとします。既にroomIdに依存するEffectがあるため、そこに分析呼び出しを追加したいと思うかもしれません。

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

しかし、後で接続を再確立する必要がある別の依存関係をこのEffectに追加すると想像してみてください。このEffectが再同期すると、logVisit(roomId)も、意図していない同じルームに対して呼び出されます。訪問のログ記録は、接続とは別個のプロセスです。これらを2つの独立したEffectとして記述してください。

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}

コード内の各Effectは、個別の独立した同期プロセスを表す必要があります。

上記の例では、1つのEffectを削除しても、他のEffectのロジックは壊れません。これは、異なるものを同期していることを示す良い指標であり、それらを分割することが理にかなっていることを意味します。一方、まとまりのあるロジックを別々のEffectに分割すると、コードは「よりクリーン」に見えるかもしれませんが、保守がより困難になります。そのため、コードの見栄えではなく、プロセスが同じか別々かどうかを検討する必要があります。

Effectは、リアクティブな値に「反応」します。

Effectは2つの変数(serverUrlroomId)を読み取りますが、roomIdのみを依存関係として指定しました。

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

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

なぜserverUrlを依存関係にする必要がないのでしょうか?

これは、serverUrlが再レンダリングによって変更されることがないためです。コンポーネントが何回再レンダリングされても、常に同じです。 serverUrlは変化しないため、依存関係として指定する意味はありません。結局のところ、依存関係は時間とともに変化するときにのみ機能するのです!

一方で、roomIdは再レンダリング時に異なる可能性があります。コンポーネント内で宣言されたprops、state、その他の値は、レンダリング中に計算され、Reactのデータフローに関与するため、リアクティブです。

serverUrlがstate変数だった場合、それはリアクティブになります。リアクティブな値は依存関係に含める必要があります。

function ChatRoom({ roomId }) { // Props change over time
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State may change over time

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// ...
}

serverUrlを依存関係に含めることで、変更後にEffectが再同期することを保証します。

このサンドボックスで選択されたチャットルームを変更するか、サーバーURLを編集してみてください。

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

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

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} />
    </>
  );
}

roomIdserverUrlのようなリアクティブな値を変更するたびに、Effectはチャットサーバーに再接続します。

空の依存関係を持つEffectの意味

serverUrlroomIdの両方をコンポーネントの外に移動した場合、どうなるでしょうか?

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

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

これで、Effectのコードはいかなるリアクティブな値も使用しなくなりました。そのため、その依存関係は空にすることができます([])。

コンポーネントの視点から考えると、空の[]依存関係配列は、このEffectがコンポーネントのマウント時にのみチャットルームに接続し、コンポーネントのアンマウント時にのみ切断することを意味します。(Reactは開発中にロジックの負荷テストのためにさらに1回再同期することに注意してください。)

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

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

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

しかし、Effectの視点から考えると、マウントとアンマウントについて考える必要はありません。重要なのは、同期を開始および停止するためにEffectが何をするかを指定したことだけです。現在、リアクティブな依存関係はありません。しかし、ユーザーが時間をかけてroomIdまたはserverUrlを変更したい場合(そしてそれらがリアクティブになる場合)、Effectのコードは変わりません。依存関係に追加するだけで済みます。

コンポーネント本体で宣言されたすべての変数はリアクティブです

propsとstateだけがリアクティブな値ではありません。それらから計算する値もリアクティブです。propsまたはstateが変更されると、コンポーネントは再レンダリングされ、それらから計算された値も変更されます。これが、Effectで使用されるコンポーネント本体のすべての変数をEffectの依存関係リストに含める必要がある理由です。

ユーザーがドロップダウンでチャットサーバーを選択でき、設定でデフォルトサーバーを構成することもできるとしましょう。コンテキストに設定stateを既に配置しているので、そのコンテキストからsettingsを読み取ります。そして、propsから選択されたサーバーとデフォルトサーバーに基づいてserverUrlを計算します。

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}

この例では、serverUrlはpropでもstate変数でもありません。レンダリング中に計算する通常の変数です。しかし、レンダリング中に計算されるため、再レンダリングによって変更される可能性があります。これがリアクティブである理由です。

コンポーネント内のすべての値(props、state、コンポーネント本体の変数を含む)はリアクティブです。リアクティブな値は再レンダリング時に変更される可能性があるため、リアクティブな値をEffectの依存関係として含める必要があります。

言い換えれば、Effectはコンポーネント本体のすべての値に「反応」します。

詳細

グローバル変数または変更可能な値を依存関係にすることができますか?

変更可能な値(グローバル変数を含む)はリアクティブではありません。

location.pathnameのような変更可能な値は、依存関係にすることはできません。 変更可能であるため、Reactのレンダリングデータフローの外でいつでも完全に変更される可能性があります。変更しても、コンポーネントの再レンダリングはトリガーされません。したがって、依存関係に指定した場合でも、Reactは変更時にEffectを再同期する必要があることを認識しません。これは、レンダリング中に変更可能なデータを読み取る(依存関係を計算する際)とレンダリングの純粋性が破られるため、Reactのルールにも違反します。代わりに、useSyncExternalStoreを使用して、外部の変更可能な値を読み取り、購読する必要があります。

ref.currentのような変更可能な値、またはそこから読み取るものも、依存関係にすることはできません。useRefによって返されるrefオブジェクト自体は依存関係にすることができますが、そのcurrentプロパティは意図的に変更可能です。これにより、再レンダリングをトリガーせずに何かを追跡できます。しかし、変更しても再レンダリングがトリガーされないため、リアクティブな値ではなく、変更時にEffectを再実行する必要があることをReactは認識しません。

このページの下で学習するように、リンターはこれらの問題を自動的にチェックします。

Reactは、すべてのリアクティブな値を依存関係として指定したことを確認します

リンターがReact用に設定されている場合、Effectのコードで使用されるすべてのリアクティブな値がその依存関係として宣言されていることをチェックします。例えば、roomIdserverUrlの両方がリアクティブであるため、これはlintエラーです。

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

function ChatRoom({ roomId }) { // roomId is reactive
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Something's wrong here!

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

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} />
    </>
  );
}

これはReactエラーのように見えるかもしれませんが、実際にはReactがコードのバグを指摘しています。`roomId`と`serverUrl`はどちらも時間とともに変化する可能性がありますが、それらが変化したときにEffectを再同期することを忘れてしまっています。ユーザーがUIで異なる値を選択した後でも、最初の`roomId`と`serverUrl`に接続されたままになります。

このバグを修正するには、リンターの提案に従って、`roomId`と`serverUrl`をEffectの依存関係として指定してください。

function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ All dependencies declared
// ...
}

上記のサンドボックスでこの修正を試してみてください。リンターエラーが解消され、必要に応じてチャットが再接続されることを確認してください。

注記

場合によっては、Reactはコンポーネント内で宣言されている場合でも、値が変化しないことを認識しています。たとえば、`useState`から返される`set`関数と`useRef`から返されるrefオブジェクトは安定しています。つまり、再レンダリング時に変更されることが保証されていません。安定した値はリアクティブではないため、リストから省略できます。含めても問題ありません。変更されないため、影響はありません。

再同期したくない場合の対処法

前の例では、`roomId`と`serverUrl`を依存関係としてリストすることで、lintエラーを修正しました。

しかし、代わりにリンターに対して、これらの値がリアクティブな値ではない、つまり再レンダリングの結果として変化しないことを「証明」することもできます。 たとえば、`serverUrl`と`roomId`がレンダリングに依存せず、常に同じ値を持つ場合、それらをコンポーネントの外に移動できます。これで、依存関係である必要がなくなります。

const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive

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

Effectの中に移動することもできます。レンダリング中に計算されないため、リアクティブではありません。

function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Effectはリアクティブなコードブロックです。 これらは、内部で読み取る値が変更されると再同期されます。相互作用ごとに一度だけ実行されるイベントハンドラーとは異なり、Effectは同期が必要なときにいつでも実行されます。

依存関係を「選択」することはできません。 依存関係には、Effect内で読み取るすべてのリアクティブな値を含める必要があります。リンターはこれを強制します。場合によっては、無限ループやEffectの再同期が頻繁になりすぎるなどの問題につながる可能性があります。リンターを抑制することでこれらの問題を解決しないでください!代わりに、次のことを試してください。

落とし穴

リンターはあなたの友人ですが、その能力は限られています。リンターは、依存関係が間違っている場合にのみ認識します。各ケースを解決するための最良の方法を知るわけではありません。リンターが依存関係を提案するが、それを追加するとループが発生する場合、リンターを無視するという意味ではありません。その値がリアクティブではなく、依存関係である必要がないように、Effectの内側(または外側)のコードを変更する必要があります。

既存のコードベースがある場合、次のようにリンターを抑制するEffectがいくつかあるかもしれません。

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

次のページ ページでは、ルールを破ることなくこのコードを修正する方法を学習します。修正する価値は常にあります!

要約

  • コンポーネントは、マウント、更新、アンマウントできます。
  • 各Effectは、周囲のコンポーネントとは別々のライフサイクルを持ちます。
  • 各Effectは、開始および停止できる個別の同期プロセスを表します。
  • Effectの記述と読み取りを行うときは、コンポーネントの観点(マウント、更新、またはアンマウントの方法)ではなく、個々のEffectの観点(同期の開始と停止の方法)から考えてください。
  • コンポーネント本体内で宣言された値は「リアクティブ」です。
  • リアクティブな値は、時間とともに変化する可能性があるため、Effectを再同期する必要があります。
  • リンターは、Effect内で使用されるすべてのリアクティブな値が依存関係として指定されていることを検証します。
  • リンターによってフラグ付けされたすべてのエラーは正当です。ルールを破ることなくコードを修正する方法は常にあります。

課題 1 5:
キーストロークごとに再接続する問題の修正

この例では、ChatRoomコンポーネントは、コンポーネントのマウント時にチャットルームに接続し、アンマウント時に切断し、別のチャットルームを選択すると再接続します。この動作は正しいので、機能するように維持する必要があります。

しかし、問題があります。下部にあるメッセージボックスに入力するたびに、ChatRoomもチャットに再接続します。(コンソールをクリアして入力してみればわかります)。この問題を修正して、これが発生しないようにしてください。

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

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

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

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  });

  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} />
    </>
  );
}