エフェクトの依存関係を削除する
エフェクトを記述すると、リンターはエフェクトが読み取るすべてのリアクティブな値(propsやstateなど)がエフェクトの依存関係のリストに含まれていることを確認します。これにより、エフェクトがコンポーネントの最新のpropsやstateと同期し続けることが保証されます。不要な依存関係があると、エフェクトが頻繁に実行されたり、無限ループが発生したりする可能性があります。このガイドに従って、エフェクトから不要な依存関係を確認し削除してください。
学習内容
- エフェクトの依存関係による無限ループの修正方法
- 依存関係を削除したい場合の対処法
- エフェクトから値を読み取る際にそれに「反応」しない方法
- オブジェクトと関数の依存関係を避ける方法とその理由
- 依存関係リンターを抑制することが危険な理由とその代わりに行うべきこと
依存関係はコードと一致するべき
エフェクトを記述するときは、まずエフェクトで行いたいことの開始と停止の方法を指定します:
次に、エフェクトの依存関係を空([])にしておくと、リンターが正しい依存関係を提案します:
リンターの指示に従って依存関係を記入します:
エフェクトはリアクティブな値に「反応」します。 roomIdはリアクティブな値(再レンダリングによって変更される可能性がある)であるため、リンターはそれが依存関係として指定されていることを確認します。roomIdが異なる値を受け取ると、Reactはエフェクトを再同期します。これにより、チャットが選択されたルームに接続されたままになり、ドロップダウンに「反応」することが保証されます:
依存関係を削除するには、それが依存関係ではないことを証明する
Effectの依存関係を「選択」することはできないことに注意してください。Effectのコードで使用されるすべてのリアクティブな値は、依存関係リストで宣言する必要があります。依存関係リストは、周囲のコードによって決定されます:
リアクティブな値には、propsとコンポーネント内で直接宣言されたすべての変数や関数が含まれます。roomIdはリアクティブな値であるため、依存関係リストから削除することはできません。リンターはそれを許可しません:
そしてリンターは正しいのです!roomIdは時間の経過とともに変化する可能性があるため、これによりコードにバグが生じるでしょう。
依存関係を削除するには、リンターに対してそれが依存関係である必要がないことを「証明」します。例えば、roomIdをコンポーネントの外に移動して、それがリアクティブではなく、再レンダリング時に変化しないことを証明できます:
これでroomIdはリアクティブな値ではなくなり(再レンダリング時に変化しません)、依存関係である必要はなくなりました:
これが、空の([])依存配列を指定できるようになった理由です。あなたのEffectはもはやリアクティブな値に依存していないので、コンポーネントのpropsやstateが変更されたときに再実行する必要がまったくありません。
依存関係を変更するには、コードを変更する
あなたのワークフローには、あるパターンがあることに気づいたかもしれません:
- まず、Effectのコードやリアクティブな値の宣言方法を変更します。
- 次に、リンターに従って、変更したコードに合わせて依存関係を調整します。
- 依存関係のリストに満足できない場合は、最初のステップに戻り(コードを再度変更します)。
最後の部分が重要です。依存関係を変更したい場合は、まず周囲のコードを変更してください。依存配列は、Effectのコードで使用されているすべてのリアクティブな値のリストとして考えることができます選択するのではありません。そのリストはあなたのコードを記述しているのです。依存配列を変更するには、コードを変更してください。
これは方程式を解くような感覚かもしれません。目標(例えば、依存関係を削除する)から始めて、その目標に合致するコードを「見つける」必要があります。方程式を解くのが楽しいと思う人は多くないでしょうし、Effectを書くことについても同じことが言えるかもしれません!幸いなことに、以下に試せる一般的なレシピのリストがあります。
落とし穴
既存のコードベースがある場合、以下のようにリンターを抑制するEffectがいくつかあるかもしれません:
依存関係がコードと一致しない場合、バグを導入するリスクが非常に高くなります。リンターを抑制することで、Effectが依存する値についてReactに「嘘」をつくことになります。
代わりに、以下のテクニックを使用してください。
不要な依存関係の削除
Effectの依存関係をコードに合わせて調整するたびに、依存配列を見てください。これらの依存関係のいずれかが変更されたときにEffectが再実行されることは理にかなっていますか?時には、答えは「いいえ」です:
- Effectの異なる部分を、異なる条件下で再実行したい場合があるかもしれません。異なる部分を異なる条件下で再実行したい場合があるかもしれません。
- ある依存関係の最新の値だけを読み取り、その変更に「反応」したくない場合があるかもしれません。
- 依存関係がオブジェクトや関数であるために、意図せず頻繁に変更される可能性があります。
適切な解決策を見つけるには、Effectに関するいくつかの質問に答える必要があります。それらを順に見ていきましょう。
このコードはイベントハンドラに移動すべきか?
最初に考えるべきことは、このコードがそもそもEffectであるべきかどうかです。
フォームを想像してください。送信時に、submitted状態変数をtrueに設定します。POSTリクエストを送信し、通知を表示する必要があります。このロジックを、submittedがtrueであることに「反応」するEffectの中に置きました:
後で、通知メッセージを現在のテーマに応じてスタイル付けしたいので、現在のテーマを読み取ります。themeはコンポーネント本体で宣言されているため、これはリアクティブな値です。そのため、依存関係として追加します:
これにより、バグが発生しました。まずフォームを送信し、その後ダークテーマとライトテーマを切り替えることを想像してください。themeが変更され、Effectが再実行され、同じ通知が再び表示されます!
ここでの問題は、これがそもそもEffectであるべきではないということです。このPOSTリクエストを送信し、通知を表示したいのは、フォームの送信という特定のインタラクションに応答してです。特定のインタラクションに応答してコードを実行するには、そのロジックを対応するイベントハンドラに直接置きます:
コードがイベントハンドラ内にあるため、リアクティブではなくなり、ユーザーがフォームを送信したときにのみ実行されます。イベントハンドラとEffectの選択と不要なEffectの削除方法について詳しく読んでください。
あなたのEffectは複数の無関係なことを行っているか?
次に自問すべき質問は、あなたのEffectが複数の無関係なことを行っているかどうかです。
ユーザーが都市と地域を選択する必要がある配送フォームを作成していると想像してください。選択されたcountryに応じて、サーバーからcitiesのリストを取得し、ドロップダウンに表示します:
これは、Effectでのデータ取得の良い例です。cities状態を、countryプロップに応じてネットワークと同期しています。これはイベントハンドラではできません。ShippingFormが表示された直後、およびcountryが変更されるたびに(どのインタラクションが原因であっても)取得する必要があるからです。
次に、都市の地域用の2つ目の選択ボックスを追加するとします。これは現在選択されているcityのareasを取得する必要があります。同じEffect内に2つ目のfetch呼び出しを追加することから始めるかもしれません:
しかし、このEffectがcity状態変数を使用するようになったため、依存配列にcityを追加する必要がありました。これにより、問題が発生します。ユーザーが別の都市を選択すると、Effectが再実行され、fetchCities(country)が呼び出されます。その結果、都市のリストを何度も不必要に再取得することになります。
このコードの問題点は、2つの異なる無関係なものを同期させていることです:
-
cities状態をcountryプロップに基づいてネットワークと同期させたい。 -
areas状態をcity状態に基づいてネットワークと同期させたい。
ロジックを2つのEffectに分割し、それぞれが同期する必要のあるプロップに反応するようにします:
これで、最初のEffectはcountryが変更されたときのみ再実行され、2番目のEffectはcityが変更されたときに再実行されます。目的によって分離しました:2つの異なるものが2つの別々のEffectによって同期されます。2つの別々のEffectは2つの別々の依存配列を持つため、意図せずにお互いをトリガーすることはありません。
最終的なコードは元のコードよりも長くなりますが、これらのEffectを分割することは依然として正しいです。各Effectは独立した同期プロセスを表すべきです。この例では、1つのEffectを削除しても、もう1つのEffectのロジックが壊れることはありません。これは、それらが異なるものを同期していることを意味し、分割することは良いことです。重複が気になる場合は、繰り返しのロジックをカスタムフックに抽出することでこのコードを改善できます。
次の状態を計算するために、ある状態を読み取っていますか?
このEffectは、新しいメッセージが到着するたびに、新しく作成された配列でmessages状態変数を更新します:
これは、messages変数を使用して、新しい配列を作成し、既存のすべてのメッセージから始まり、末尾に新しいメッセージを追加します。しかし、messagesはEffectによって読み取られるリアクティブな値であるため、依存関係にする必要があります:
そして、messagesを依存関係にすると問題が発生します。
メッセージを受信するたびに、setMessages()によって、受信したメッセージを含む新しいmessages配列でコンポーネントが再レンダリングされます。しかし、このEffectがmessagesに依存するようになったため、これによってEffectも再同期されます。つまり、新しいメッセージが来るたびにチャットが再接続されます。ユーザーはそれを望まないでしょう!
この問題を修正するには、Effect内でmessagesを読み取らないでください。代わりに、更新関数をsetMessagesに渡します:
ここで、あなたのEffectがmessages変数を全く読み取らなくなったことに注目してください。必要なのは、msgs => [...msgs, receivedMessage]のような更新関数を渡すことだけですあなたの更新関数をキューに入れ、次のレンダリング時にmsgs引数を提供します。これが、Effect自体がmessagesに依存する必要がなくなった理由です。この修正の結果、チャットメッセージを受信してもチャットが再接続されることはなくなります。
値の変更に「反応」せずに値を読み取りたいですか?
ユーザーが新しいメッセージを受信したときに音を再生したいが、isMutedがtrueの場合は除く、と仮定します:
Effectがコード内でisMutedを使用するようになったため、依存関係に追加する必要があります:
問題は、isMutedが変更されるたびに(例えば、ユーザーが「ミュート」トグルを押したとき)、Effectが再同期され、チャットに再接続することです。これは望ましいユーザー体験ではありません!(この例では、リンターを無効にしても機能しません。そうすると、isMutedが古い値で「固着」してしまいます。)
この問題を解決するには、リアクティブであってはならないロジックをEffectから抽出する必要があります。このEffectがisMutedの変更に「反応」してほしくないのです。この非リアクティブなロジックをEffect Eventに移動します:
Effect Eventを使用すると、Effectをリアクティブな部分(roomIdのようなリアクティブな値とその変更に「反応」すべき部分)と非リアクティブな部分(onMessageがisMutedを読み取るように、最新の値のみを読み取る部分)に分割できます。Effect Event内でisMutedを読み取るようになったため、Effectの依存関係である必要はありません。その結果、「ミュート」設定をオン・オフしてもチャットは再接続されず、元の問題が解決されます!
propsからのイベントハンドラーのラップ
コンポーネントがイベントハンドラーをpropとして受け取る場合、同様の問題に遭遇することがあります:
親コンポーネントが毎回のレンダリングで異なるonReceiveMessage関数を渡すと仮定します:
依存関係であるonReceiveMessageは、親コンポーネントが再レンダリングするたびにEffectを再同期させ、チャットに再接続させてしまいます。これを解決するには、呼び出しをEffect Eventでラップします:
Effect Eventはリアクティブではないため、依存関係として指定する必要はありません。その結果、親コンポーネントが毎回の再レンダリングで異なる関数を渡しても、チャットは再接続されなくなります。
リアクティブなコードと非リアクティブなコードの分離
この例では、roomIdが変更されるたびに訪問を記録したいとします。各ログに現在のnotificationCountを含めたいですが、したくないのは、notificationCountの変更がログイベントをトリガーすることです。
解決策は、再び非リアクティブなコードをEffect Eventに分割することです:
あなたのロジックはroomIdに対してリアクティブに動作してほしいので、Effect内でroomIdを読み取ります。しかし、notificationCountの変更によって余分な訪問がログに記録されることは望まないので、notificationCountはEffect Event内で読み取ります。Effect Eventを使用してEffectsから最新のpropsとstateを読み取る方法について詳しく学ぶ。
意図せずにリアクティブな値が変化していませんか?
例えば、コンポーネントの本体でoptionsオブジェクトを作成し、そのオブジェクトをEffect内で読み取る場合を考えてみましょう:
このオブジェクトはコンポーネント本体で宣言されているため、リアクティブな値です。Effect内でこのようなリアクティブな値を読み取る場合、依存関係として宣言します。これにより、Effectがその変化に「反応」することが保証されます:
依存関係として宣言することは重要です!これにより、例えばroomIdが変更された場合に、Effectが新しいoptionsでチャットに再接続することが保証されます。しかし、上記のコードには問題もあります。それを確認するために、以下のサンドボックスの入力欄にタイプして、コンソールで何が起こるか見てみましょう:
上記のサンドボックスでは、入力欄はmessage状態変数のみを更新します。ユーザーの視点から見ると、これはチャット接続に影響を与えるべきではありません。しかし、messageを更新するたびに、コンポーネントが再レンダリングされます。コンポーネントが再レンダリングされると、その内部のコードが最初から再実行されます。
新しいoptionsオブジェクトは、ChatRoomコンポーネントが再レンダリングされるたびに一から作成されます。Reactは、optionsオブジェクトが前回のレンダリングで作成された異なるオブジェクトであると認識します。これが、Effect(optionsオブジェクトとはoptionsに依存している)を再同期させ、タイピングするたびにチャットが再接続される理由です。
この問題はオブジェクトと関数にのみ影響します。JavaScriptでは、新しく作成された各オブジェクトと関数は、他のすべてのものとは異なるものと見なされます。その内部の内容が同じであっても関係ありません!
オブジェクトや関数の依存関係は、Effectが必要以上に再同期される原因になることがあります。
そのため、可能な限り、Effectの依存関係としてオブジェクトや関数を使用することは避けるべきです。代わりに、それらをコンポーネントの外側、Effectの内部に移動したり、そこからプリミティブな値を抽出したりすることを試みてください。
静的なオブジェクトと関数をコンポーネントの外に移動する
オブジェクトがpropsやstateに依存しない場合、そのオブジェクトをコンポーネントの外に移動できます:
これにより、リンターにそれがリアクティブでないことを証明します。再レンダリングの結果として変化することはないため、依存関係である必要はありません。これで、ChatRoomを再レンダリングしても、Effectが再同期されることはありません。
これは関数にも適用されます:
関数createOptionsはコンポーネントの外で宣言されているため、リアクティブな値ではありません。これが、Effectの依存関係として指定する必要がなく、Effectが再同期されることも決してない理由です。
動的なオブジェクトと関数をEffectの内部に移動する
オブジェクトが、roomIdプロップのように、再レンダリングの結果として変化する可能性のあるリアクティブな値に依存している場合、それをコンポーネントの外側に引き出すことはできません。しかし、その作成をEffectのコードの内部に移動することはできます:
これで、optionsはEffectの内部で宣言されているため、Effectの依存関係ではなくなりました。代わりに、Effectが使用する唯一のリアクティブな値はroomIdです。roomIdはオブジェクトや関数ではないため、それが意図せず異なることはないと確信できます。JavaScriptでは、数値と文字列はその内容によって比較されます:
この修正により、入力欄を編集してもチャットが再接続されることはなくなりました:
しかし、確かに、予想通り、roomIdのドロップダウンを変更すると再接続されます。
これは関数でも機能します:
Effect内のロジックをまとめるために独自の関数を書くことができます。それらをEffectの内部で宣言する限り、それらはリアクティブな値ではなく、したがってEffectの依存関係である必要はありません。
オブジェクトからプリミティブ値を読み取る
時々、propsからオブジェクトを受け取ることがあります:
ここでのリスクは、親コンポーネントがレンダリング中にオブジェクトを作成することです:
これにより、親コンポーネントが再レンダリングするたびにEffectが再接続されます。これを修正するには、Effectの外部でオブジェクトから情報を読み取り、オブジェクトや関数の依存関係を持たないようにします:
ロジックは少し繰り返しになります(Effectの外部でオブジェクトから値を読み取り、Effectの内部で同じ値を持つオブジェクトを作成します)。しかし、これによりEffectが実際に依存している情報が非常に明確になります。親コンポーネントが意図せずにオブジェクトを再作成しても、チャットは再接続されません。しかし、options.roomIdまたはoptions.serverUrlが実際に異なる場合、チャットは再接続されます。
関数からプリミティブ値を計算する
同じアプローチが関数でも機能します。例えば、親コンポーネントが関数を渡すとします:
依存関係にしない(再レンダリング時に再接続を引き起こさない)ためには、Effectの外部で呼び出します。これにより、オブジェクトではないroomIdとserverUrlの値が得られ、Effect内部から読み取ることができます:
これは純粋関数に対してのみ機能します。なぜなら、それらはレンダリング中に安全に呼び出せるからです。関数がイベントハンドラーであり、その変更によってEffectが再同期されるのを避けたい場合は、代わりにEffect Eventでラップしてください。
まとめ
- 依存関係は常にコードと一致させるべきです。
- 依存関係に不満がある場合、編集すべきはコードです。
- リンターを抑制すると非常にわかりにくいバグの原因となり、常に避けるべきです。
- 依存関係を削除するには、リンターにそれが不要であることを「証明」する必要があります。
- 特定のインタラクションに応じてコードを実行する必要がある場合は、そのコードをイベントハンドラーに移動します。
- Effectの異なる部分が異なる理由で再実行されるべき場合は、複数のEffectに分割します。
- 前の状態に基づいて状態を更新したい場合は、更新関数を渡します。
- 最新の値を「反応」させずに読み取りたい場合は、EffectからEffect Eventを抽出します。
- JavaScriptでは、オブジェクトと関数は作成された時期が異なると、異なるものと見なされます。
- オブジェクトと関数の依存関係は避けるようにしてください。コンポーネントの外側またはEffectの内側に移動します。
Try out some challenges
Challenge 1 of 4:Fix a resetting interval #
This Effect sets up an interval that ticks every second. You’ve noticed something strange happening: it seems like the interval gets destroyed and re-created every time it ticks. Fix the code so that the interval doesn’t get constantly re-created.
