Effect依存関係の削除

Effectを記述する際、リンターはEffectが読み取るすべてのリアクティブな値(プロップスや状態など)がEffectの依存関係のリストに含まれていることを検証します。これにより、Effectがコンポーネントの最新のプロップスと状態と同期した状態を維持できます。不要な依存関係があると、Effectが頻繁に実行されたり、無限ループが発生したりする可能性があります。このガイドに従って、Effectから不要な依存関係を確認して削除してください。

学習内容

  • 無限Effect依存ループの修正方法
  • 依存関係を削除する場合の対処法
  • リアクティブでない値をEffectから読み取る方法と理由
  • オブジェクトと関数依存関係を回避する方法と理由
  • 依存関係リンターの抑制が危険な理由、およびその代わりにすべきこと

依存関係はコードと一致する必要があります

Effectを記述する際には、まず、Effectに実行させたい処理の開始と停止方法を指定します。

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

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

次に、Effectの依存関係を空のままにする([])と、リンターが正しい依存関係を提案します。

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();
  }, []); // <-- Fix the mistake here!
  return <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} />
    </>
  );
}

リンターの指示に従って入力してください

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

Effectはリアクティブな値に「反応」します。 roomIdはリアクティブな値(再レンダリングによって変更される可能性があります)であるため、リンターはそれを依存関係として指定していることを検証します。roomIdが異なる値を受け取ると、ReactはEffectを再同期します。これにより、チャットが選択したルームに接続された状態を維持し、ドロップダウンに「反応」します。

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

依存関係を削除するには、それが依存関係ではないことを証明します

Effectの依存関係は「選択」できません。Effectのコードで使用されるすべてのリアクティブな値は、依存関係リストに宣言する必要があります。依存関係リストは、周囲のコードによって決定されます。

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

function ChatRoom({ roomId }) { // This is a reactive value
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
// ...
}

リアクティブな値には、プロップスと、コンポーネント内で直接宣言されたすべての変数と関数が含まれます。roomIdはリアクティブな値であるため、依存関係リストから削除することはできません。リンターは許可しません。

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

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'roomId'
// ...
}

そして、リンターは正しいでしょう!roomIdは時間の経過とともに変化する可能性があるため、これによりコードにバグが発生します。

依存関係を削除するには、リンターに対して、それが依存関係である必要がないことを「証明」します。たとえば、roomIdをコンポーネントの外に移動して、それがリアクティブではなく、再レンダリング時に変化しないことを証明できます。

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore

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

これで、roomIdはリアクティブな値ではなくなり(再レンダリング時に変更されません)、依存関係である必要がなくなりました。

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

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

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

そのため、空の([])依存関係リストを指定できるようになりました。Effectはもはやリアクティブな値に依存していないため、コンポーネントのプロップスや状態が変更されたときに再実行する必要がなくなりました。

依存関係を変更するには、コードを変更します

ワークフローにパターンがあることに気付かれたかもしれません。

  1. まず、Effectのコード、またはリアクティブな値の宣言方法を変更します。
  2. 次に、リンターに従って、依存関係を変更したコードに合わせます。
  3. 依存関係のリストに満足できない場合は、最初のステップに戻って(コードを再度変更します)。

最後の部分は重要です。依存関係を変更する場合は、まず周囲のコードを変更してください。依存関係リストは、Effectのコードで使用されているすべてのリアクティブな値のリスト と考えることができます。そのリストに何を置くか選択するのではありません。リストはコードを記述しています。依存関係リストを変更するには、コードを変更します。

これは方程式を解くような感覚かもしれません。目標(たとえば、依存関係を削除するなど)から始め、その目標に一致するコードを「見つける」必要があります。方程式を解くのが楽しいとは限らず、Effectの作成についても同様です。幸いにも、以下に試すことができる一般的なレシピのリストがあります。

落とし穴

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

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

依存関係がコードと一致しない場合、バグが発生するリスクが非常に高くなります。リンターを抑制することで、Effectが依存する値についてReactに「嘘をつく」ことになります。

代わりに、以下の手法を使用してください。

詳細解説

なぜ依存関係リンターの抑制は危険なのでしょうか?

リンターの抑制は、見つけにくく修正が困難な非常に直感に反するバグにつながります。以下に1つの例を示します。

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

「マウント時のみ」Effectを実行したいとしましょう。空の([])依存関係 がそれを行うと読んだので、リンターを無視して、[] を強制的に依存関係として指定することにしました。

このカウンターは、2つのボタンで設定可能な量だけ、毎秒1ずつ増加するはずでした。しかし、このEffectが何にも依存していないとReactに「嘘をついた」ため、Reactは最初のレンダリングからのonTick関数を常に使用し続けます。そのレンダリング中、count0で、increment1でした。これが、そのレンダリングからのonTickが常に毎秒setCount(0 + 1)を呼び出す理由であり、常に1が表示される理由です。このようなバグは、複数のコンポーネントにまたがっている場合、修正が困難になります。

リンターを無視するよりも常に良い解決策があります!このコードを修正するには、onTickを依存関係リストに追加する必要があります。(間隔が一度だけ設定されるようにするには、onTickをEffectイベントにする必要があります。)

依存関係のlintエラーはコンパイルエラーとして扱うことをお勧めします。抑制しないと、このようなバグは発生しません。このページの残りの部分では、これおよびその他のケースの代替案について説明します。

不要な依存関係の削除

Effectの依存関係をコードに反映させるたびに、依存関係リストを確認してください。これらの依存関係のいずれかが変更されたときに、Effectを再実行することが理にかなっていますか?答えが「いいえ」の場合もあります。

  • Effectの異なる部分を異なる条件下で再実行したい場合があります。
  • 変更に「反応」するのではなく、一部の依存関係の最新の値のみを読み取りたい場合があります。
  • オブジェクトや関数であるため、依存関係が意図せずに頻繁に変更される場合があります。

適切な解決策を見つけるには、Effectに関するいくつかの質問に答える必要があります。それらを順に見ていきましょう。

このコードをイベントハンドラーに移動する必要がありますか?

まず考えるべきことは、このコードがそもそもEffectであるべきかどうかです。

フォームを想像してみてください。送信時に、submitted状態変数をtrueに設定します。POSTリクエストを送信し、通知を表示する必要があります。submittedtrueであることに「反応する」Effectの中にこのロジックを入れました。

function Form() {
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

後で、現在のテーマに合わせて通知メッセージのスタイルを設定したいので、現在のテーマを読み取ります。themeはコンポーネント本体で宣言されているため、リアクティブな値であり、依存関係として追加されます。

function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ All dependencies declared

function handleSubmit() {
setSubmitted(true);
}

// ...
}

これにより、バグが発生します。まずフォームを送信してから、ダークテーマとライトテーマを切り替えるとします。themeが変更され、Effectが再実行され、同じ通知が再び表示されます!

ここでの問題は、これがそもそもEffectであるべきではないことです。このPOSTリクエストを送信し、通知を表示するには、フォームの送信という特定のインタラクションに応じて行う必要があります。特定のインタラクションに応じてコードを実行するには、そのロジックを対応するイベントハンドラーに直接配置します。

function Form() {
const theme = useContext(ThemeContext);

function handleSubmit() {
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!', theme);
}

// ...
}

コードがイベントハンドラ内にあるため、リアクティブではありません。そのため、ユーザーがフォームを送信したときのみ実行されます。イベントハンドラとEffectsの使い分け不要なEffectsの削除方法について詳しくはご覧ください。

Effectが複数の無関係な処理を行っていませんか?

次に自問自答すべき点は、Effectが複数の無関係な処理を行っているかどうかです。

ユーザーが都市と地域を選択する必要がある配送フォームを作成していると想像してください。選択されたに応じて、サーバーから都市のリストを取得して、ドロップダウンに表示します。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

// ...

これはEffectでデータを取得する良い例です。プロパティに従って、都市の状態をネットワークと同期します。これはイベントハンドラでは実行できません。ShippingFormが表示された直後と、が変更されるたびに(どのようなインタラクションによって変更されたかに関わらず)フェッチする必要があるためです。

次に、現在選択されている都市地域を取得する必要がある、都市地域のセレクトボックスを2つ追加するとします。同じEffect内に地域のリストを取得するための2番目のfetch呼び出しを追加することから始めるかもしれません。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared

// ...

しかし、Effectが都市状態変数を使用するようになったため、依存関係のリストに都市を追加する必要がありました。これにより、問題が発生しました。ユーザーが別の都市を選択すると、Effectが再実行され、fetchCities(country)が呼び出されます。その結果、都市のリストを何度も不必要に再取得することになります。

このコードの問題点は、2つの異なる無関係なものを同期していることです。

  1. プロパティに基づいて、都市の状態をネットワークと同期したいと考えています。
  2. 都市の状態に基づいて、地域の状態をネットワークと同期したいと考えています。

ロジックを2つのEffectに分割し、それぞれが同期する必要があるプロパティに反応するようにします。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared

// ...

これで、最初のEffectはが変更された場合のみ再実行され、2番目のEffectは都市が変更された場合に再実行されます。目的別に分離しました。2つの異なるものが2つの独立したEffectによって同期されます。2つの独立したEffectには2つの独立した依存関係リストがあるため、意図せず互いをトリガーすることはありません。

最終的なコードは元のコードよりも長くなりましたが、これらのEffectを分割することは依然として正しい方法です。各Effectは独立した同期プロセスを表す必要があります。この例では、1つのEffectを削除しても、もう1つのEffectのロジックは壊れません。つまり、異なるものを同期するため、分割するのが適切です。重複が気になる場合は、繰り返し処理をカスタムフックに抽出することで、このコードを改善できます。

次の状態を計算するために、ある状態を読み込んでいますか?

このEffectは、新しいメッセージが到着するたびに、新しく作成された配列を使用してmessages状態変数を更新します。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...

messages変数を使用して新しい配列を作成し、既存のメッセージをすべて含め、最後に新しいメッセージを追加します。しかし、messagesはEffectによって読み取られるリアクティブな値であるため、依存関係である必要があります。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...

そして、messagesを依存関係にすることで問題が発生します。

メッセージを受信するたびに、setMessages()によって、受信したメッセージを含む新しいmessages配列を使用してコンポーネントが再レンダリングされます。しかし、このEffectはmessagesに依存するようになったため、これもEffectを再同期します。そのため、新しいメッセージごとにチャットが再接続されます。ユーザーはそれを望んでいません!

この問題を解決するには、Effect内でmessagesを読み取らないようにします。代わりに、更新関数setMessagesに渡します。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effectがmessages変数をまったく読み取らないことに注目してください。msgs => [...msgs, receivedMessage]のような更新関数のみを渡す必要があります。Reactは更新関数をキューに入れます。そして、次のレンダリング時にmsgs引数を渡します。これが、Effect自体がmessagesに依存する必要がない理由です。この修正の結果、チャットメッセージを受信しても、チャットは再接続されなくなります。

値を読み取る際に、その変化に「反応」せずに済ませたいですか?

開発中

このセクションでは、Reactの安定版ではまだリリースされていない実験的なAPIについて説明します。

ユーザーが新しいメッセージを受信したときに、isMutedtrueでない限り、サウンドを再生したいとします。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...

Effectがコード内でisMutedを使用するようになったため、依存関係に追加する必要があります。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ All dependencies declared
// ...

問題は、isMutedが変更されるたびに(たとえば、ユーザーが「ミュート」トグルを押したとき)、Effectが再同期され、チャットに再接続されることです。これは、望ましいユーザーエクスペリエンスではありません!(この例では、リンターを無効にしても機能しません。そうすると、isMutedは古い値のまま「固定」されます。)

この問題を解決するには、反応性の必要がないロジックをEffectから抽出する必要があります。isMutedの変化にこのEffectを「反応」させたくありません。この非反応性のロジック部分をEffectイベントに移動しましょう:

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effectイベントを使用すると、Effectを反応性のある部分(roomIdなどの反応性のある値とその変化に「反応」する必要がある部分)と反応性のない部分(onMessageisMutedを読み取るように、最新の値のみを読み取る部分)に分割できます。Effectイベント内でisMutedを読み取るようになったため、Effectの依存関係にする必要はありません。 その結果、「ミュート」設定のオンオフを切り替えてもチャットは再接続されなくなり、元の問題が解決されます!

propsからイベントハンドラーをラップする

コンポーネントがpropsとしてイベントハンドラーを受け取る場合にも、同様の問題が発生する可能性があります。

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ All dependencies declared
// ...

親コンポーネントが、レンダリングごとに異なるonReceiveMessage関数を渡すとします。

<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>

onReceiveMessageは依存関係であるため、親コンポーネントの再レンダリングごとにEffectが再同期され、チャットが再接続されます。これを解決するには、呼び出しをEffectイベントでラップします。

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effectイベントは反応性がないため、依存関係として指定する必要はありません。その結果、親コンポーネントがレンダリングごとに異なる関数を渡しても、チャットは再接続されなくなります。

反応性のあるコードと反応性のないコードの分離

この例では、roomIdが変更されるたびに訪問をログに記録したいと考えています。現在のnotificationCountをすべてのログに含めたいのですが、notificationCountの変更によってログイベントがトリガーされるのは望んでいません。

解決策は、再び非反応性のコードをEffectイベントに分割することです。

function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});

useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}

roomIdに関してロジックを反応性を持たせたいので、Effect内でroomIdを読み取ります。ただし、notificationCountの変更によって余分な訪問がログに記録されるのは望んでいないため、Effectイベント内でnotificationCountを読み取ります。Effectイベントを使用して、Effectから最新のpropsとstateを読み取る方法の詳細については、こちらをご覧ください。

反応性のある値が意図せず変更されていますか?

Effectに特定の値に「反応」させたい場合もありますが、その値が望ましいよりも頻繁に変更される場合があり、ユーザーの視点からは実際の変更を反映していない可能性があります。たとえば、コンポーネントの本体内でoptionsオブジェクトを作成し、そのオブジェクトをEffect内から読み取るとします。

function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...

このオブジェクトはコンポーネントの本体で宣言されているため、反応性のある値です。Effect内でこのような反応性のある値を読み取るときは、依存関係として宣言します。これにより、Effectがその変更に「反応」することが保証されます。

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

依存関係として宣言することが重要です!これにより、たとえばroomIdが変更された場合、Effectは新しいoptionsを使用してチャットに再接続します。ただし、上記のコードにも問題があります。それを確認するには、下のサンドボックスの入力に文字を入力して、コンソールで何が起こるかを確認してください。

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

上記のサンドボックスでは、入力はmessage状態変数のみを更新します。ユーザーの視点からは、これはチャット接続に影響を与えるべきではありません。しかし、messageを更新するたびに、コンポーネントが再レンダリングされます。コンポーネントが再レンダリングされると、その中のコードが最初から実行されます。

ChatRoomコンポーネントの再レンダリングごとに、新しいoptionsオブジェクトが最初から作成されます。Reactは、optionsオブジェクトが前回のレンダリング時に作成されたoptionsオブジェクトとは異なるオブジェクトであると認識します。これが、Effect(optionsに依存)を再同期させ、入力時にチャットが再接続される理由です。

この問題は、オブジェクトと関数にのみ影響します。JavaScriptでは、新しく作成されたオブジェクトと関数は、他のすべてのオブジェクトと関数とは別個のものと見なされます。それらの内部の内容が同じであっても問題ありません!

// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

オブジェクトと関数の依存関係は、必要以上にEffectの再同期を頻繁に行う可能性があります。

そのため、可能な限り、Effectの依存関係としてオブジェクトと関数を避けるようにしてください。代わりに、それらをコンポーネントの外側、Effectの内側、またはそれらからプリミティブ値を抽出するようにしてください。

コンポーネントの外に静的なオブジェクトと関数を移動する

オブジェクトがpropsとstateに依存していない場合、そのオブジェクトをコンポーネントの外に移動できます。

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
// ...

このようにすることで、リンターに対してそれがリアクティブではないことを証明できます。再レンダリングの結果として変更されることはないので、依存関係である必要はありません。これで、ChatRoomの再レンダリングによって、Effectが再同期されることはありません。

これは関数にも有効です。

function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}

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

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

createOptionsがコンポーネントの外で宣言されているため、リアクティブな値ではありません。そのため、Effectの依存関係に指定する必要がなく、Effectが再同期されることはありません。

動的なオブジェクトと関数をEffectの中に移動する

オブジェクトがroomId propのように、再レンダリングの結果として変更される可能性のあるリアクティブな値に依存する場合、コンポーネントのに移動することはできません。ただし、Effectのコードのにその作成を移動することはできます。

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
// ...

これで、optionsはEffect内で宣言されるため、Effectの依存関係ではなくなります。代わりに、Effectで使用される唯一のリアクティブな値はroomIdです。roomIdはオブジェクトでも関数でもないため、意図せず異なる値になることはありません。JavaScriptでは、数値と文字列は内容によって比較されます。

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

この修正により、入力を編集してもチャットが再接続されなくなりました。

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

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]);

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

ただし、予想通り、roomIdのドロップダウンを変更すると、再接続されます。

これは関数にも有効です。

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

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

useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}

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

Effect内でロジックの断片をグループ化するための独自の関数を記述できます。同様にEffect内で宣言する限り、それらはリアクティブな値ではないため、Effectの依存関係である必要はありません。

オブジェクトからプリミティブ値を読み取る

場合によっては、propsからオブジェクトを受け取ることもあります。

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

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

ここでのリスクは、親コンポーネントがレンダリング中にオブジェクトを作成することです。

<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>

これにより、親コンポーネントが再レンダリングされるたびにEffectが再接続されます。これを修正するには、Effectの外でオブジェクトから情報を読み取り、オブジェクトと関数の依存関係を持たないようにします。

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

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

ロジックはやや反復的になります(Effectの外でオブジェクトからいくつかの値を読み取り、同じ値を持つオブジェクトをEffect内で作成します)。しかし、Effectが実際にどのような情報に依存しているかを明確にします。オブジェクトが親コンポーネントによって意図せずに再作成された場合でも、チャットは再接続されません。ただし、options.roomIdまたはoptions.serverUrlが実際に異なる場合、チャットは再接続されます。

関数からプリミティブ値を計算する

同じアプローチは関数にも有効です。例えば、親コンポーネントが関数を渡すとします。

<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>

それを依存関係にし(そして再レンダリング時に再接続させる)のを避けるために、Effectの外で呼び出します。これにより、オブジェクトではなく、Effectの内側から読み取ることができるroomIdserverUrlの値が得られます。

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

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

これは、レンダリング中に安全に呼び出すことができる純粋な関数にのみ有効です。関数がイベントハンドラーであるものの、その変更によってEffectが再同期されるのを望まない場合は、代わりにEffectイベントでラップしてください。

要約

  • 依存関係は常にコードと一致する必要があります。
  • 依存関係に不満がある場合は、コードを編集する必要があります。
  • リンターを抑制すると、非常に分かりにくいバグが発生するため、常に避けるべきです。
  • 依存関係を削除するには、リンターに対してそれが不要であることを「証明」する必要があります。
  • 特定のインタラクションに応じてコードを実行する必要がある場合は、そのコードをイベントハンドラーに移動します。
  • Effectの異なる部分が異なる理由で再実行される必要がある場合は、それを複数のEffectに分割します。
  • 以前の状態に基づいて状態を更新する場合は、更新関数を使用します。
  • 最新の値を「反応」せずに読み取る場合は、EffectからEffectイベントを抽出します。
  • JavaScriptでは、異なる時間に作成されたオブジェクトと関数は、異なるものと見なされます。
  • オブジェクトと関数の依存関係は避けてください。コンポーネントの外またはEffectの内側に移動してください。

課題 1 4:
リセット間隔の修正

このエフェクトは、毎秒ティックする間隔を設定します。奇妙な現象に気づかれたかもしれません。間隔はティックするたびに破棄され、再作成されているようです。間隔が絶えず再作成されないようにコードを修正してください。

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Counter: {count}</h1>
}