React Docs Effectの依存関係を取り除く
Removing Effect Dependencies
Effectを書くとき、リンターはEffectが読み取るすべてのリアクティブな値(props
やstate
など)が依存関係リストに含まれていることを確認する。これにより、Effectがコンポーネントの最新のprops
やstate
に同期していることが保証される。不要な依存関係があるとEffectが頻繁に実行されたり、無限ループになったりすることがあるため、以下のガイドに従ってEffectsの不要な依存関係を取り除く。
You will learn
- Effectの無限依存ループを修正する方法
- 依存関係を削除したいときの対処法
- Effectから"反応"させずに価値を読み取る方法
- オブジェクトと関数の依存関係を回避する方法と理由
- 依存性リンターを抑制することが危険な理由と、その代わりに何をすべきなのか
依存関係はコードと一致させる
Effectを書くときはまず、動作を開始・停止する方法を指定する。
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
// 開始
const connection = createConnection(serverUrl, roomId);
connection.connect();
// 停止
return () => connection.disconnect();
// ...
}
そして、Effect dependenciesを空([])にしておくと、リンターが正しい依存関係を提案してくれるため、そのように修正する。
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
Effectsはリアクティブな値に"反応"し、ここでは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
// ...
}
リアクティブな値にはprops
やコンポーネント内部で直接宣言されたすべての変数と関数が含まれる。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を外に出すことでそれがリアクティブでなく、再レンダリングで変化しないことを証明することができる。こうすることで、Effectはどのリアクティブな値にも依存せず、コンポーネントのどのprop
やstate
が変化しても再実行する必要がなくなるため、空の依存関係リスト([])を指定することができる。
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
// ...
}
依存関係を変更するにはコードを変更する
依存関係を変更するにはコードを変更する。
ワークフローにパターンがあることには気づいているだろうか。
- まず、Effectのコードやリアクティブ値の宣言方法を変更する
- 次にリンターにしたがって変更したコードに一致するように依存関係を調整する
- 依存関係のリストに不満があれば最初のステップに戻って再度コードを変更する
依存関係リストはEffectのコードが使用するすべてのリアクティブな値のリストと考えることができ、そのリストを変更したい場合はEffectのコードを修正する必要がある。依存関係リストに何を記述するかはこちらが決めることではない。
これは方程式を解くような感覚かもしれず、ある目標(例えば依存関係を取り除くなど)からスタートしてその目標に合致するコードを見つける必要がある。
不要な依存関係を取り除く
コードを修正し、Effectの依存関係を調整する度に、それらの依存関係のいずれかが変更されたときにEffectの再実行が必要か確認し、時には必要ない場合もあるだろう。
- Effectの異なる部分を異なる条件で再実行したい場合
- 依存関係の変更に反応するのではなく依存関係の最新の値だけを読みたいという場合
- 依存関係がオブジェクトや関数であるため、意図せず頻繁に変更される可能性がある場合
正しい解決策を見つけるにはEffectに関するいくつかの質問に答える必要がある。
このイベントはイベントハンドラに移すべきか?
まず考えるのはコードがEffectであるべきかどうかである。
送信時にsubmit
ステートをtrue
に変更、POSTリクエスト送信、通知を表示する必要があるフォームがあるとする。
これらのロジックをsubmit
がtrue
であることに反応する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);
}
// ...
}
その後、現在のテーマに従って通知メッセージのスタイルを変更することになったため、現在のテーマを読み込むようにする。テーマはコンポーネントで宣言されているリアクティブな値であるため依存関係として追加する。
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);
}
// ...
}
最初にフォームを送信し、その後テーマを切り替えるとその変更により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);
}
// ...
}
こうすることで、ユーザーがフォームを送信したときだけ実行されるようになる。イベントハンドラとEffectの使い分けや不要なEffectを削除する方法についてはそれぞれのリンク先で説明。
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
// ...
これはcountry prop
に従って都市のステートをネットワークと同期させており、Effectでデータを取得するいい例である。ShippingForm
が表示され、国が変わるたびにフェッチする必要があるため、イベントハンドラで同じことはできない。
ここで現在選択されている都市の地域をフェッチするために、都市の地域用セレクトボックスを追加するとする。まず、同じEffectの中にある地域リストに対してのフェッチ処理を追加する。
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がcity state
を使用することになるため、依存関係にcity
を追加する必要があるが、これだとユーザーが別の都市を選択するとEffectが再実行され、不要に何度も都市リストを再フェッチすることになる。
このコードの問題点は以下2つの異なる無関係なものを同期させていることである。
-
country prop
に基づいてcity
のstate
をネットワークに同期させたい -
city prop
に基づいてareas
のstate
をネットワークに同期させたい
これらを2つのEffectsに分割し、それぞれ関係するprop
によって再実行されるようにする。
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// countryに基づいてcityを同期させる
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);
// cityに基づいてareasを同期させる
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
// ...
こうすることで、1つ目のEffectは国が変わったときだけ再実行され、2つ目のEffectは都市が変わったときだけ再実行されるようになる。2つの異なるものを2つの別々のEffectsで同期させるという目的別にわけている。
最終的なコードは修正前に比べると長くなってしまうが各Effectは独立した同期処理を表す必要があるため、これらのEffectsを分割することは正しい。コードの重複が気になる場合は繰り返しのロジックをカスタムHookに抽出することで改善できる。
次のstateを計算するためにあるstateを読み取っているか?
以下Effectは新しいメッセージが届くたびに新しく作成した配列でmessages state
を更新している。
messages state
を使って既存のすべてのメッセージで始まる新しい配列を作成し、最後に新しいメッセージを追加しているが、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
によって受信したメッセージを含む新しいメッセージでコンポーネントが再レンダリングされるが、Effectはメッセージに依存するようになったためEffectの再同期も行われるため、チャットの再接続も行われるようになる。
この問題を解決するにはEffect内部でメッセージを読み込むのではなくsetMessages
にupdater
関数を渡すようにする。
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 state
を読み込む必要がなくなり、msgs => [...msgs, receivedMessage]
のようにupdater
関数を渡すだけで良くなる。Reactはupdater
関数をキューに入れ、次のレンダリング時にmsgs
の引数を渡すため、Effect自身がメッセージに依存する必要がなくなった。
値の変化に反応せずに読み解くか?
React安定板でまだリリースしていないAPIを含む
例えばユーザーが新しいメッセージを受信したときに、isMuted
がfalse
であれば音声を再生したいとする。
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が再同期し、チャットに再接続されることである。この問題を解決するには、反応させるべきでないロジックをEffectから抽出する必要がある。このEffectはisMuted
の変化に対して反応させたくないためそのロジックをEffect Eventに移動させる。
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 Eventを使うと、Effectをリアクティブな部分(roomId
やその変化に反応する部分)とリアクティブでない部分(onMessage
がisMuted
を見るように最新の値だけを読む部分)に分割できる。Effect Eventの中でisMuted
を読むようになったため、Effectの依存関係である必要はなくなる。
propsからイベントハンドラをラッピングする
コンポーネントがイベントハンドラをprops
として受け取る場合にも同様の問題に遭遇することがある。
親コンポーネントがレンダリングごとに異なるonReceiveMessage
を渡すとする。
<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>
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
は依存関係に含まれるため、親の再レンダリングの度にEffectが再実行され、チャットに再接続することになる。これを解決するにはこの呼び出しをEffect Eventでラップする。
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 Eventはリアクティブではないため、依存関係として指定する必要はなく、親コンポーネントが再レンダリングごとに異なる関数を渡してもチャットが再接続されることはなくなる。
リアクティブコードとそうでないコードの切り分け
以下の例ではroomId
が変更されるたびにページへの訪問を記録したいとする。すべてのログにnotificationCount
を含めたいが、notificationCount
の変更がログイベントをトリガーすることは望んでいない。この解決策もEffect Eventに分割することである。
function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});
useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}
roomId
に関してロジックをリアクティブにしたいため、 Effectの内部でroomId
をonVisit
の引数として渡すが、notificationCount
の変更ではログを送信したくないためEffect Eventの内部でnotificationCount
を読み取る。Effect Eventを使ってEffectから最新のprops
とstate
を読み取る方法については別ページで説明。
リアクティブな値が意図せずに変化することがある?
ある値に対してEffectを反応させたいが、その値が思ったよりも頻繁に変化し、ユーザー視点から実際の変化を反映しない場合がある。例えば、コンポーネント内でオプションオブジェクトを作成し、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が新しいオプションでチャットに再接続されることが保証されるが、このコードには問題がある。
入力フォームはメッセージのステートを更新するだけで、ユーザー視点から見るとチャット接続に影響を与えないはずだが、メッセージを更新するたびいにコンポーネントが再レンダリングされ、その中のコードが一から実行される。
ChatRoom
コンポーネントを再レンダリングするたびに新しいオプションオブジェクトがゼロから作成される。Reactはオブジェクトが前回レンダリングされたものと異なるオブジェクトであると認識し、Effectがが再実行される。
この問題は、オブジェクトと関数にのみ影響する。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
に依存していない場合、そのオブジェクトをコンポーネントの外に移動させることができる。
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
createOptions
はコンポーネントの外で宣言されるため、リアクティブな値ではないため、Effectの依存関係で指定する必要はなく、Effectの再実行を引き起こすこともない。
Effecttの中でダイナミックなオブジェクトや関数を動かす
オブジェクトが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
// ...
Effectの内部でoptions
が宣言されたことで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(() => {
// Effect内でオブジェクトを宣言
const options = {
serverUrl: serverUrl,
roomId: roomId
};
// 関数でも同じことができる
// function createOptions() {
// return {
// serverUrl: serverUrl,
// roomId: roomId
// };
// }
const options = createOptions();
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)} />
</>
);
}
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の内側で同じ値を持つオブジェクトを作成する)が、Efectが実際に依存する情報を非常に明確にすることができる。親コンポーネントによって意図せずオブジェクトが再作成された場合、チャットは再接続されないが、options.roomId
やoptions.serverUrl
が本当に異なる場合は再接続される。
関数からプリミティブ値を算出する
同じ方法は関数でも有効であり、例えば親コンポーネントが関数を渡すとする。
<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>
依存関係になるのを避けるため(再レンダリング時に再実行が発生するため)、Effectの外で呼び出す。これにより、オブジェクトではないroomId
とserverUrl
の値が得られ、Effectの内部から読み取ることができるようになる。
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 Eventにラップすればよい。
Discussion