useEffect
は、コンポーネントを外部システムと同期させるための React フックです。
useEffect(setup, dependencies?)
リファレンス
useEffect(setup, dependencies?)
Effect を宣言するには、コンポーネントのトップレベルで useEffect
を呼び出します。
import { 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();
};
}, [serverUrl, roomId]);
// ...
}
パラメータ
-
setup
: Effect のロジックを含む関数。setup 関数は、オプションで *クリーンアップ* 関数を返すこともできます。コンポーネントが DOM に追加されると、React は setup 関数を実行します。依存関係が変更された状態で再レンダリングされるたびに、React はまず古い値でクリーンアップ関数 (提供した場合) を実行し、次に新しい値で setup 関数を実行します。コンポーネントが DOM から削除された後、React はクリーンアップ関数を実行します。 -
省略可能
dependencies
:setup
コード内で参照されるすべてのリアクティブな値のリスト。リアクティブな値には、props、state、およびコンポーネント本体内で直接宣言されたすべての変数と関数が含まれます。リンターがReact 用に設定されている場合、すべてのリアクティブな値が依存関係として正しく指定されていることを確認します。依存関係のリストは、項目の数が一定でなければならず、[dep1, dep2, dep3]
のようにインラインで記述する必要があります。React は、Object.is
比較を使用して、各依存関係をその前の値と比較します。この引数を省略すると、Effect はコンポーネントの再レンダリングごとに再実行されます。依存関係の配列を渡す場合、空の配列を渡す場合、および依存関係をまったく渡さない場合の差異についてはこちらをご覧ください。
戻り値
useEffect
は undefined
を返します。
注意点
-
useEffect
はフックなので、コンポーネントのトップレベルまたは独自のフック内でのみ呼び出すことができます。ループや条件の中で呼び出すことはできません。もし必要な場合は、新しいコンポーネントを抽出し、その中に状態を移動してください。 -
もしあなたが外部システムとの同期を試みていないのであれば、Effect は必要ないかもしれません。
-
Strict Mode が有効な場合、React は最初の実際のセットアップの前に、開発時のみの追加のセットアップ + クリーンアップサイクルを1回実行します。これは、クリーンアップロジックがセットアップロジックを「反映」し、セットアップが行っていることを停止または元に戻すことを保証するためのストレステストです。これにより問題が発生する場合は、クリーンアップ関数を実装してください。
-
依存関係の一部がコンポーネント内で定義されたオブジェクトまたは関数の場合、Effect が必要以上に頻繁に再実行されるリスクがあります。これを修正するには、不要なオブジェクトと関数の依存関係を削除してください。また、状態の更新を抽出したり、リアクティブでないロジックを Effect の外部に移動することもできます。
-
Effect が(クリックのような)インタラクションによって引き起こされなかった場合、React は通常、Effect を実行する前に、更新された画面を最初に描画させます。Effect が(例えばツールチップの位置決めのように)視覚的な何かを実行していて、遅延が顕著(例えば、ちらつく)な場合は、
useEffect
をuseLayoutEffect
に置き換えてください。 -
Effect が(クリックのような)インタラクションによって引き起こされた場合、React はブラウザが更新された画面を描画する前に Effect を実行する可能性があります。これにより、Effect の結果がイベントシステムによって観測されることが保証されます。通常、これは期待どおりに動作します。ただし、
alert()
のように、描画後まで作業を延期する必要がある場合は、setTimeout
を使用できます。詳細については、reactwg/react-18/128 を参照してください。 -
Effect が(クリックのような)インタラクションによって引き起こされた場合でも、React は、Effect 内の状態更新を処理する前に、ブラウザに画面の再描画を許可する可能性があります。通常、これは期待どおりに動作します。ただし、ブラウザによる画面の再描画をブロックする必要がある場合は、
useEffect
をuseLayoutEffect
に置き換える必要があります。 -
Effect はクライアントでのみ実行されます。サーバーレンダリング中には実行されません。
使用法
外部システムへの接続
一部のコンポーネントは、ページに表示されている間、ネットワーク、一部のブラウザ API、またはサードパーティライブラリに接続を維持する必要があります。これらのシステムは React によって制御されないため、外部と呼ばれます。
コンポーネントを外部システムに接続するには、コンポーネントのトップレベルで useEffect
を呼び出します。
import { 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();
};
}, [serverUrl, roomId]);
// ...
}
useEffect
には 2 つの引数を渡す必要があります。
- そのシステムに接続するセットアップコードを持つセットアップ関数。
- そのシステムから切断するクリーンアップコードを持つクリーンアップ関数を返す必要があります。
- これらの関数内で使用されるコンポーネントからのすべての値を含む依存関係のリスト。
React は必要に応じてセットアップ関数とクリーンアップ関数を呼び出します。これは複数回発生する可能性があります。
- コンポーネントがページに追加されたとき(マウント時)に、セットアップコードが実行されます。
- 依存関係が変更されたコンポーネントのすべての再レンダリングの後。
- まず、古い props と状態を持つ クリーンアップコードが実行されます。
- 次に、新しい props と状態を持つ セットアップコードが実行されます。
- コンポーネントがページから削除された後(アンマウント時)に、クリーンアップコードが最後に1回実行されます。
上記の例でこのシーケンスを説明しましょう。
上記の ChatRoom
コンポーネントがページに追加されると、初期の serverUrl
と roomId
でチャットルームに接続します。serverUrl
または roomId
のいずれかが再レンダリングの結果として変更された場合(たとえば、ユーザーがドロップダウンで別のチャットルームを選択した場合)、Effect は前のルームから切断し、次のルームに接続します。 ChatRoom
コンポーネントがページから削除されると、Effect は最後に1回切断します。
開発時に React がセットアップとクリーンアップをセットアップの前に1回余分に実行するのは、バグを見つけやすくするためです。 これは、Effect のロジックが正しく実装されていることを検証するストレステストです。これにより視覚的な問題が発生する場合は、クリーンアップ関数にロジックが欠落しています。クリーンアップ関数は、セットアップ関数が行っていたことを停止または元に戻す必要があります。経験則として、ユーザーは、(本番環境のように)セットアップが1回呼び出される場合と、セットアップ → クリーンアップ → セットアップシーケンス(開発環境のように)の場合を区別できないようにする必要があります。一般的な解決策を参照してください。
すべての Effect を独立したプロセスとして記述し、各 Effect を個別の同期プロセスとして捉え、一度に1つのセットアップ/クリーンアップサイクルについて考えるようにしてください。コンポーネントのマウント、更新、アンマウントのいずれであっても、問題にならないはずです。クリーンアップロジックがセットアップロジックを正しく「反映」していれば、Effect は必要なだけ頻繁にセットアップとクリーンアップを実行しても、耐性があります。
例 1の 5: チャットサーバーへの接続
この例では、ChatRoom
コンポーネントは、chat.js
で定義された外部システムに接続を維持するために Effect を使用しています。「チャットを開く」を押すと、ChatRoom
コンポーネントが表示されます。このサンドボックスは開発モードで実行されるため、ここで説明したように、余分な接続と切断のサイクルが発生します。ドロップダウンと入力を使用して roomId
と serverUrl
を変更し、Effect がチャットに再接続する様子を確認してください。「チャットを閉じる」を押すと、Effect が最後に切断される様子を確認できます。
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'); 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} />} </> ); }
Effect をカスタムフックでラップする
Effect は 「エスケープハッチ」です。React から「外に出る」必要があり、ユースケースに適した組み込みの解決策がない場合に使用します。手動で Effect を記述する必要が頻繁にある場合、通常はコンポーネントが依存する一般的な動作のために、いくつかのカスタムフックを抽出する必要があるという兆候です。
例えば、この useChatRoom
カスタムフックは、より宣言的な API の背後に Effect のロジックを「隠蔽」します。
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
これにより、次のような任意のコンポーネントから使用できます。
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
また、React エコシステムでは、あらゆる目的に対応した優れたカスタムフックが数多く存在します。
例 1の 3: カスタム useChatRoom
フック
この例は、以前の例の1つと同一ですが、ロジックがカスタムフックに抽出されています。
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useChatRoom({ roomId: roomId, serverUrl: 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'); 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} />} </> ); }
React以外のウィジェットの制御
場合によっては、外部システムをコンポーネントのpropsやstateに同期させたい場合があります。
例えば、Reactを使用せずに記述されたサードパーティ製のマップウィジェットやビデオプレーヤーコンポーネントがある場合、Effectを使用して、その状態をReactコンポーネントの現在の状態に一致させるメソッドを呼び出すことができます。このEffectは、map-widget.js
で定義されている MapWidget
クラスのインスタンスを作成します。Map
コンポーネントの zoomLevel
propを変更すると、Effectはクラスインスタンスで setZoom()
を呼び出して、同期を維持します。
import { useRef, useEffect } from 'react'; import { MapWidget } from './map-widget.js'; export default function Map({ zoomLevel }) { const containerRef = useRef(null); const mapRef = useRef(null); useEffect(() => { if (mapRef.current === null) { mapRef.current = new MapWidget(containerRef.current); } const map = mapRef.current; map.setZoom(zoomLevel); }, [zoomLevel]); return ( <div style={{ width: 200, height: 200 }} ref={containerRef} /> ); }
この例では、MapWidget
クラスは渡されたDOMノードのみを管理するため、クリーンアップ関数は必要ありません。Map
Reactコンポーネントがツリーから削除されると、DOMノードと MapWidget
クラスインスタンスの両方がブラウザのJavaScriptエンジンによって自動的にガベージコレクションされます。
Effectsを使用したデータの取得
Effectを使用して、コンポーネントのデータを取得できます。フレームワークを使用している場合、フレームワークのデータ取得メカニズムを使用する方が、Effectを手動で記述するよりもはるかに効率的であることに注意してください。
Effectから手動でデータを取得する場合は、コードは次のようになります。
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
// ...
ignore
変数は false
に初期化され、クリーンアップ中に true
に設定されることに注意してください。これにより、コードが「競合状態」に陥らないようにします。ネットワークの応答は、送信した順序とは異なる順序で到着する可能性があります。
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { let ignore = false; setBio(null); fetchBio(person).then(result => { if (!ignore) { setBio(result); } }); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ?? 'Loading...'}</i></p> </> ); }
async
/ await
構文を使用して書き換えることもできますが、クリーンアップ関数は依然として提供する必要があります。
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { async function startFetching() { setBio(null); const result = await fetchBio(person); if (!ignore) { setBio(result); } } let ignore = false; startFetching(); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ?? 'Loading...'}</i></p> </> ); }
Effectに直接データ取得を記述すると、繰り返しが多くなり、後でキャッシュやサーバーレンダリングなどの最適化を追加することが難しくなります。独自のカスタムフック、またはコミュニティによってメンテナンスされているカスタムフックを使用する方が簡単です。
深掘り
Effects内で fetch
呼び出しを記述することは、特に完全にクライアントサイドのアプリでは、データを取得する一般的な方法です。ただし、これは非常に手動的なアプローチであり、重大な欠点があります。
- Effectはサーバーでは実行されません。つまり、最初にサーバーでレンダリングされたHTMLには、データのないローディング状態のみが含まれます。クライアントコンピュータは、すべてのJavaScriptをダウンロードしてアプリをレンダリングし、そこで初めてデータのロードが必要であることを認識します。これはあまり効率的ではありません。
- Effectで直接取得すると、「ネットワークウォーターフォール」を簡単に作成できます。親コンポーネントをレンダリングし、いくつかのデータを取得し、子コンポーネントをレンダリングしてから、子コンポーネントがデータの取得を開始します。ネットワークがそれほど高速でない場合、これはすべてのデータを並行して取得するよりも大幅に遅くなります。
- Effectで直接取得することは、通常、データのプリロードやキャッシュを行わないことを意味します。たとえば、コンポーネントがアンマウントされてから再びマウントした場合、データを再度取得する必要があります。
- あまり人間工学的ではありません。競合状態などのバグが発生しないように
fetch
呼び出しを記述する場合、かなりのボイラープレートコードが関与します。
この欠点のリストはReactに固有のものではありません。これは、任意のライブラリでマウント時にデータを取得する場合に当てはまります。ルーティングと同様に、データ取得を適切に行うことは簡単ではないため、次のアプローチをお勧めします。
- フレームワークを使用している場合は、組み込みのデータ取得メカニズムを使用します。最新のReactフレームワークには、効率的で、上記の落とし穴が発生しない統合されたデータ取得メカニズムがあります。
- そうでない場合は、クライアントサイドキャッシュの使用または構築を検討してください。 一般的なオープンソースソリューションとしては、React Query、useSWR、そしてReact Router 6.4+などがあります。独自のソリューションを構築することもできます。その場合、内部ではEffectsを使用しますが、リクエストの重複排除、レスポンスのキャッシュ、ネットワークウォーターフォールを避けるためのロジック(データの事前ロードやルートへのデータ要件の持ち上げなど)も追加することになります。
これらのアプローチのいずれも適さない場合は、引き続きEffectsで直接データをフェッチできます。
リアクティブな依存関係の指定
Effectの依存関係を「選択」することはできません。 Effectのコードで使用されるすべてのリアクティブな値は、依存関係として宣言する必要があります。Effectの依存関係リストは、周囲のコードによって決定されます。
function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
}
serverUrl
またはroomId
のいずれかが変更されると、Effectは新しい値を使用してチャットに再接続します。
リアクティブな値には、propsと、コンポーネント内で直接宣言されたすべての変数と関数が含まれます。 roomId
とserverUrl
はリアクティブな値であるため、依存関係から削除することはできません。それらを省略しようとして、リンターがReact用に正しく構成されている場合、リンターはこれを修正する必要がある間違いとしてフラグを立てます。
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
}
依存関係を削除するには、リンターに対して、それが依存関係である必要がないことを「証明」する必要があります。 たとえば、serverUrl
をコンポーネントの外に移動することで、それがリアクティブではなく、再レンダリング時に変更されないことを証明できます。
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
serverUrl
がリアクティブな値ではなくなった(そして再レンダリング時に変更できない)ため、依存関係である必要はありません。Effectのコードがリアクティブな値をまったく使用していない場合、その依存関係リストは空である必要があります([]
):
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
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は、コンポーネントのpropsまたは状態が変更されても再実行されません。
例 1の 3: 依存関係の配列を渡す
依存関係を指定すると、Effectは最初のレンダリング後および変更された依存関係で再レンダリングされた後に実行されます。
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different
以下の例では、serverUrl
とroomId
はリアクティブな値であるため、両方とも依存関係として指定する必要があります。その結果、ドロップダウンで別の部屋を選択するか、サーバーURLの入力を編集すると、チャットが再接続されます。ただし、message
はEffectで使用されていない(したがって依存関係ではない)ため、メッセージを編集してもチャットに再接続されません。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [serverUrl, roomId]); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> <label> Your message:{' '} <input value={message} onChange={e => setMessage(e.target.value)} /> </label> </> ); } export default function App() { const [show, setShow] = useState(false); 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> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> </label> {show && <hr />} {show && <ChatRoom roomId={roomId}/>} </> ); }
Effectからの前の状態に基づいて状態を更新する
Effectからの前の状態に基づいて状態を更新したい場合、問題が発生する可能性があります。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
}
count
はリアクティブな値であるため、依存関係のリストに指定する必要があります。ただし、これにより、count
が変更されるたびにEffectがクリーンアップされて再度設定されることになります。これは理想的ではありません。
これを修正するには、c => c + 1
状態更新関数をsetCount
に渡します。
import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(c => c + 1); // ✅ Pass a state updater }, 1000); return () => clearInterval(intervalId); }, []); // ✅ Now count is not a dependency return <h1>{count}</h1>; }
count + 1
の代わりにc => c + 1
を渡すようになったため、Effectはもはやcount
に依存する必要はありません。この修正の結果、count
が変更されるたびに、インターバルをクリーンアップして再度設定する必要がなくなります。
不要なオブジェクトの依存関係を削除する
もしあなたの Effect が、レンダリング中に作成されたオブジェクトや関数に依存している場合、Effect が過剰に実行される可能性があります。例えば、以下の Effect は、options
オブジェクトがレンダリングごとに異なるため、レンダリングのたびに再接続します。
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
レンダリング中に作成されたオブジェクトを依存関係として使用することは避けてください。代わりに、Effect の内部でオブジェクトを作成してください。
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} /> </> ); }
Effect の内部で options
オブジェクトを作成するようにしたことで、Effect 自体は roomId
文字列にのみ依存するようになりました。
この修正により、入力欄への入力がチャットの再接続を引き起こすことはありません。再作成されるオブジェクトとは異なり、roomId
のような文字列は、別の値を設定しない限り変更されません。依存関係の削除について詳しくはこちらをご覧ください。
不要な関数依存関係の削除
もしあなたの Effect が、レンダリング中に作成されたオブジェクトや関数に依存している場合、Effect が過剰に実行される可能性があります。例えば、以下の Effect は、createOptions
関数がレンダリングごとに異なるため、レンダリングのたびに再接続します。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
再レンダリングごとに関数をゼロから作成すること自体は問題ではありません。それを最適化する必要はありません。しかし、それを Effect の依存関係として使用すると、再レンダリングのたびに Effect が再実行されます。
レンダリング中に作成された関数を依存関係として使用することは避けてください。代わりに、Effect の内部で関数を宣言してください。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; 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]); 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} /> </> ); }
Effect の内部で createOptions
関数を定義するようにしたことで、Effect 自体は roomId
文字列にのみ依存するようになりました。この修正により、入力欄への入力がチャットの再接続を引き起こすことはありません。再作成される関数とは異なり、roomId
のような文字列は、別の値を設定しない限り変更されません。依存関係の削除について詳しくはこちらをご覧ください。
Effect から最新の props と state を読み取る
デフォルトでは、Effect からリアクティブな値を読み取る場合、それを依存関係として追加する必要があります。これにより、Effect がその値のすべての変更に「反応」することが保証されます。ほとんどの依存関係では、それが望ましい動作です。
しかし、時には、Effect から最新の props と state を読み取りたいが、それらに「反応」したくない場合があります。 例えば、ページ訪問ごとにショッピングカート内のアイテム数をログに記録したいと想像してください。
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}
もし、url
の変更ごとに新しいページ訪問をログに記録したいが、shoppingCart
だけが変更された場合はログに記録したくないとしたらどうでしょうか? リアクティビティのルールを破ることなく、shoppingCart
を依存関係から除外することはできません。ただし、Effect の内部から呼び出されている場合でも、コードの一部が変更に「反応」することを望まないと表現することができます。Effect Eventを宣言し、useEffectEvent
Hook を使用して、shoppingCart
を読み取るコードをその中に移動します。
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Effect Event はリアクティブではなく、常に Effect の依存関係から除外する必要があります。 これにより、非リアクティブなコード(一部の props や state の最新の値を読み取ることができる場所)をそれらの中に配置できます。onVisit
の内部で shoppingCart
を読み取ることで、shoppingCart
が Effect を再実行しないようにします。
Effect Event がリアクティブなコードと非リアクティブなコードを分離する方法について詳しくはこちらをご覧ください。
サーバーとクライアントで異なるコンテンツを表示する
アプリでサーバーレンダリング(直接またはフレームワーク経由)を使用する場合、コンポーネントは 2 つの異なる環境でレンダリングされます。サーバー上では、最初の HTML を生成するためにレンダリングされます。クライアント上では、React はイベントハンドラーをその HTML にアタッチできるように、レンダリングコードを再度実行します。そのため、hydration が機能するためには、クライアントとサーバーで最初のレンダリング出力が同じである必要があります。
まれに、クライアントで異なるコンテンツを表示する必要がある場合があります。たとえば、アプリがlocalStorage
からデータを読み取る場合、サーバー上ではそれを実行することはできません。これがその実装方法です。
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}
アプリのロード中、ユーザーには初期レンダリング出力が表示されます。その後、ロードとハイドレーションが完了すると、Effectが実行され、didMount
がtrue
に設定され、再レンダリングがトリガーされます。これにより、クライアント専用のレンダリング出力に切り替わります。Effectはサーバーでは実行されないため、初期サーバーレンダリング中にdidMount
がfalse
だったのはそのためです。
このパターンは控えめに使用してください。回線速度が遅いユーザーは、最初のコンテンツをかなりの時間(場合によっては数秒)見ることになるため、コンポーネントの外観を大幅に変更することは避けてください。多くの場合、CSSで異なるものを条件付きで表示することで、この必要性を回避できます。
トラブルシューティング
コンポーネントのマウント時にEffectが2回実行される
厳格モードがオンの場合、開発環境では、Reactは実際のセットアップの前に、セットアップとクリーンアップを1回余分に実行します。
これは、Effectのロジックが正しく実装されていることを検証するストレステストです。これにより目に見える問題が発生する場合は、クリーンアップ関数にロジックが欠落しています。クリーンアップ関数は、セットアップ関数が行っていたことを停止または元に戻す必要があります。経験則として、ユーザーは、(本番環境のように)セットアップが1回呼び出された場合と、(開発環境のように)セットアップ→クリーンアップ→セットアップのシーケンスの場合を区別できないようにする必要があります。
これがどのようにバグを見つけるのに役立つかとロジックを修正する方法について詳しくお読みください。
Effectがすべての再レンダリング後に実行される
まず、依存配列を指定するのを忘れていないか確認してください
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!
依存配列を指定しても、Effectがループで再実行される場合は、すべての再レンダリングで依存関係の1つが異なっているためです。
この問題をデバッグするには、依存関係をコンソールに手動でログ出力できます。
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
次に、コンソールで異なる再レンダリングからの配列を右クリックし、両方とも「グローバル変数として保存」を選択します。最初のものがtemp1
として保存され、2番目のものがtemp2
として保存されたと仮定すると、ブラウザのコンソールを使用して、両方の配列内の各依存関係が同じかどうかを確認できます。
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...
すべての再レンダリングで異なる依存関係が見つかった場合は、通常、次のいずれかの方法で修正できます。
最後の手段として(これらの方法が役に立たなかった場合)、作成をuseMemo
またはuseCallback
(関数の場合)でラップします。
Effectが無限ループで再実行される
Effectが無限ループで実行される場合、次の2つのことが当てはまる必要があります。
- Effectが何らかの状態を更新している。
- その状態が再レンダリングにつながり、Effectの依存関係が変更される。
問題を修正する前に、Effectが(DOM、ネットワーク、サードパーティのウィジェットなどの)外部システムに接続しているかどうかを自問してください。Effectが状態を設定する必要があるのはなぜですか?それは外部システムと同期していますか?それとも、それを使用してアプリケーションのデータフローを管理しようとしていますか?
外部システムがない場合は、Effectを完全に削除することでロジックが単純化されるかどうかを検討してください。
本当に外部システムと同期している場合は、Effectが状態を更新する必要がある理由と、どのような条件で状態を更新する必要があるかを考えてください。コンポーネントの視覚的な出力に影響を与える何かが変更されましたか?レンダリングで使用されないデータを追跡する必要がある場合は、ref(再レンダリングをトリガーしない)の方が適切かもしれません。Effectが状態を必要以上に更新(および再レンダリングをトリガー)しないことを確認してください。
最後に、Effectが正しいタイミングで状態を更新しているにもかかわらず、依然としてループが発生する場合は、その状態更新によってEffectの依存関係の1つが変更されているためです。依存関係の変更をデバッグする方法をお読みください。
コンポーネントがアンマウントされていないのに、クリーンアップロジックが実行される
クリーンアップ関数は、アンマウント時だけでなく、依存関係が変更されたすべての再レンダリングの前にも実行されます。さらに、開発環境では、Reactはコンポーネントのマウント直後にセットアップ+クリーンアップを1回余分に実行します。
対応するセットアップコードのないクリーンアップコードがある場合は、通常、コードのにおいがします。
useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);
クリーンアップロジックは、セットアップロジックに対して「対称」である必要があり、セットアップが行ったことを停止または元に戻す必要があります。
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
Effectのライフサイクルがコンポーネントのライフサイクルとどのように異なるかを学びましょう。
Effectが視覚的な処理を行い、実行前にちらつきが見える
Effectがブラウザによる画面の描画をブロックする必要がある場合は、useEffect
をuseLayoutEffect
に置き換えます。ただし、これはほとんどのEffectでは必要ありません。たとえば、ユーザーに表示される前にツールチップを測定して配置するなど、ブラウザの描画の前にEffectを実行することが重要な場合にのみ必要になります。