useEffect
useEffect
は、コンポーネントを外部システムと同期させるための React フックです。
useEffect(setup, dependencies?)
リファレンス
useEffect(setup, dependencies?)
コンポーネントのトップレベルで useEffect
を呼び出して、エフェクト (Effect) を宣言します。
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
: エフェクトのロジックが記述された関数です。このセットアップ関数は、オプションでクリーンアップ関数を返すことができます。コンポーネントが初めて DOM に追加されると、React はセットアップ関数を実行します。依存配列 (dependencies) が変更された再レンダー時には、React はまず古い値を使ってクリーンアップ関数(あれば)を実行し、次に新しい値を使ってセットアップ関数を実行します。コンポーネントが DOM から削除された後、React はクリーンアップ関数を最後にもう一度実行します。 -
省略可能
dependencies
:setup
コード内で参照されるすべてのリアクティブな値のリストです。リアクティブな値には、props、state、コンポーネント本体に直接宣言されたすべての変数および関数が含まれます。リンタが React 用に設定されている場合、すべてのリアクティブな値が依存値として正しく指定されているか確認できます。依存値のリストは要素数が一定である必要があり、[dep1, dep2, dep3]
のようにインラインで記述する必要があります。React は、Object.is
を使った比較で、それぞれの依存値を以前の値と比較します。この引数を省略すると、エフェクトはコンポーネントの毎回のレンダー後に再実行されます。依存値の配列を渡す場合と空の配列を渡す場合、および何も渡さない場合の違いを確認してください。
返り値
useEffect
は undefined
を返します。
注意点
-
useEffect
はフックであるため、コンポーネントのトップレベルやカスタムフック内でのみ呼び出すことができます。ループや条件文の中で呼び出すことはできません。これが必要な場合は、新しいコンポーネントを抽出し、その中に state を移動させてください。 -
外部システムと同期する必要がない場合、エフェクトはおそらく不要です。
-
Strict Mode が有効な場合、React は本物のセットアップの前に、開発時専用のセットアップ+クリーンアップサイクルを 1 回追加で実行します。これは、クリーンアップロジックがセットアップロジックと鏡のように対応しており、セットアップで行われたことを停止または元に戻していることを保証するためのストレステストです。問題が発生した場合は、クリーンアップ関数を実装します。
-
依存配列の一部にコンポーネント内で定義されたオブジェクトや関数がある場合、エフェクトが必要以上に再実行される可能性があります。これを修正するには、オブジェクト型および関数型の不要な依存値を削除します。また、エフェクトの外部に state の更新や非リアクティブなロジックを抽出することもできます。
-
エフェクトがユーザ操作(クリックなど)によって引き起こされたものでない場合、React は通常、ブラウザが新しい画面を描画した後にエフェクトを実行します。あなたのエフェクトが(ツールチップの配置など)何か視覚的な作業を行っており遅延が目立つ場合(ちらつくなど)、
useEffect
をuseLayoutEffect
に置き換えてください。 -
エフェクトがユーザ操作(クリックなど)によって引き起こされた場合、React はブラウザが更新後の画面を描画する前にエフェクトを実行することがあります。これによりエフェクトの結果がイベントシステムに見えることが保証されます。これは通常は期待通りに動作します。しかし、
alert()
のように描画後まで作業を遅らせる必要がある場合は、setTimeout
を使用できます。詳細については、reactwg/react-18/128 を参照してください。 -
エフェクトがユーザ操作(クリックなど)によって引き起こされた場合、React はエフェクト内で起きた state 更新を処理する前に、ブラウザに画面を再描画させることがあります。これは通常は期待通りに動作します。しかし、ブラウザによる画面の再描画をブロックしなければならない場合は、
useEffect
をuseLayoutEffect
に置き換える必要があります。 -
エフェクトはクライアント上でのみ実行されます。サーバレンダリング中には実行されません。
使用法
外部システムへの接続
コンポーネントによっては自身がページに表示されている間、ネットワーク、何らかのブラウザ API、またはサードパーティライブラリとの接続を維持する必要があるものがあります。これらのシステムは React によって制御されていないため、外部 (external) のものです。
コンポーネントを外部システムに接続するには、コンポーネントのトップレベルで 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 と state でクリーンアップコードが実行されます。
- 次に、新しい props と state でセットアップコードが実行されます。
- コンポーネントがページから削除(アンマウント)されると、最後にクリーンアップコードが実行されます。
上記の例でこのシーケンスを説明しましょう。
上記の ChatRoom
コンポーネントがページに追加されると、serverUrl
と roomId
の初期値を使ってチャットルームに接続します。serverUrl
または roomId
が再レンダーの結果として変更される場合(例えば、ユーザがドロップダウンで別のチャットルームを選択した場合)、あなたのエフェクトは以前のルームから切断し、次のルームに接続します。ChatRoom
コンポーネントがページから削除されると、あなたのエフェクトは最後の切断を行います。
バグを見つけ出すために、開発中には React はセットアップとクリーンアップを、セットアップの前に 1 回余分に実行します。これは、エフェクトのロジックが正しく実装されていることを確認するストレステストです。これが目に見える問題を引き起こす場合、クリーンアップ関数に一部のロジックが欠けています。クリーンアップ関数は、セットアップ関数が行っていたことを停止ないし元に戻す必要があります。基本ルールとして、ユーザはセットアップが一度しか呼ばれていない(本番環境の場合)か、セットアップ → クリーンアップ → セットアップのシーケンス(開発環境の場合)で呼ばれているかを区別できないようにする必要があります。一般的な解決法を参照してください。
各エフェクトを独立したプロセスとして記述するようにし、一回のセットアップ/クリーンアップのサイクルだけを考えるようにしてください。コンポーネントが現在マウント、更新、アンマウントのどれを行っているかを考慮すべきではありません。セットアップロジックが正しくクリーンアップロジックと「対応」されることで、エフェクトはセットアップとクリーンアップを必要に応じて何度実行しても問題が起きない、堅牢なものとなります。
例 1/5: チャットサーバへの接続
この例では、ChatRoom
コンポーネントがエフェクトを使って chat.js
で定義された外部システムに接続しています。“Open chat” を押すと ChatRoom
コンポーネントが表示されます。このサンドボックスは開発モードで実行されているため、こちらで説明されているように、接続と切断のサイクルが 1 回追加で発生します。roomId
と serverUrl
をドロップダウンと入力欄で変更して、エフェクトがチャットに再接続する様子を確認してみてください。“Close chat” を押すと、エフェクトが最後の 1 回の切断作業を行います。
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} />} </> ); }
カスタムフックにエフェクトをラップする
エフェクトは「避難ハッチ」です。React の外に出る必要があり、かつ特定のユースケースに対してより良い組み込みのソリューションがない場合に使用します。エフェクトを手で何度も書く必要があることに気付いたら、通常それは、あなたのコンポーネントが依存する共通の振る舞いのためのカスタムフックを抽出する必要があるというサインです。
例えば、この useChatRoom
カスタムフックは、エフェクトのロジックをより宣言的な API の背後に「隠蔽」します。
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
フック
この例は、これまでの例 のいずれかと同じですが、カスタムフックにロジックが抽出されています。
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 を使っていないサードパーティ製のマップウィジェットやビデオプレーヤコンポーネントがある場合、エフェクトを使ってそちらのメソッドを呼び出し、そちらの状態を React コンポーネントの現在 state に合わせることができます。以下では、map-widget.js
に定義された MapWidget
クラスのインスタンスをエフェクトが作成します。Map
コンポーネントの props である zoomLevel
が変更されると、エフェクトがクラスインスタンスの 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 ノードのみを管理しているためです。React の Map
コンポーネントがツリーから削除された後、DOM ノードと MapWidget
クラスインスタンスは、ブラウザの JavaScript エンジンによって自動的にガベージコレクションされます。
エフェクトを使ったデータフェッチ
エフェクトを使って、コンポーネントに必要なデータをフェッチ(fetch, 取得)することができます。ただしフレームワークを使用している場合は、エフェクトを自力で記述するよりも、フレームワークのデータフェッチメカニズムを使用する方がはるかに効率的であることに注意してください。
エフェクトを使って自力でデータをフェッチしたい場合は、以下のようなコードを書くことになります。
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
に設定されます。これにより、コードが “競合状態 (race condition)” に悩まされないようになります。ネットワークレスポンスは、送信した順序と異なる順序で届くことがあることに注意しましょう。
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> </> ); }
エフェクト内で直接データフェッチを書くとコードの繰り返しが増え、キャッシュやサーバレンダリングといった最適化を後から追加することが難しくなります。独自の、あるいはコミュニティがメンテナンスしているカスタムフックを使う方が簡単です。
さらに深く知る
特に完全にクライアントサイドのアプリにおいては、エフェクトの中で fetch
コールを書くことはデータフェッチの一般的な方法です。しかし、これは非常に手作業頼りのアプローチであり、大きな欠点があります。
- エフェクトはサーバ上では動作しません。これは、サーバレンダリングされた初期 HTML にはデータのないローディング中という表示のみが含まれてしまうことを意味します。クライアントのコンピュータは、すべての JavaScript をダウンロードし、アプリをレンダーした後になってやっと、今度はデータを読み込む必要もあるということに気付くことになります。これはあまり効率的ではありません。
- エフェクトで直接データフェッチを行うと、「ネットワークのウォーターフォール(滝)」を作成しやすくなります。親コンポーネントをレンダーし、それが何かデータをフェッチし、それによって子コンポーネントをレンダーし、今度はそれが何かデータのフェッチを開始する、といった具合です。ネットワークがあまり速くない場合、これはすべてのデータを並行で取得するよりもかなり遅くなります。
- エフェクト内で直接データフェッチするということはおそらくデータをプリロードもキャッシュもしていないということです。例えば、コンポーネントがアンマウントされた後に再びマウントされる場合、データを再度取得する必要があります。
- 人にとって書きやすいコードになりません。競合状態のようなバグを起こさないように
fetch
コールを書こうとすると、かなりのボイラープレートコードが必要です。
上記の欠点は、マウント時にデータをフェッチするのであれば、React に限らずどのライブラリを使う場合でも当てはまる内容です。ルーティングと同様、データフェッチの実装も上手にやろうとすると一筋縄ではいきません。私たちは以下のアプローチをお勧めします。
- フレームワークを使用している場合、組み込みのデータフェッチ機構を使用してください。モダンな React フレームワークには、効率的で上記の欠点がないデータフェッチ機構が統合されています。
- それ以外の場合は、クライアントサイドキャッシュの使用や構築を検討してください。一般的なオープンソースのソリューションには、React Query、useSWR、および React Router 6.4+ が含まれます。自分でソリューションを構築することもできます。その場合、エフェクトを内部で使用しつつ、リクエストの重複排除、レスポンスのキャッシュ、ネットワークのウォーターフォールを回避するためのロジック(データのプリロードやルーティング部へのデータ要求の巻き上げ)を追加することになります。
これらのアプローチがどちらも適合しない場合は、引き続きエフェクト内で直接データをフェッチすることができます。
リアクティブな依存配列の指定
エフェクトの依存配列は、自分で「選ぶ」たぐいの物ではないことに注意してください。エフェクトのコードによって使用されるすべてのリアクティブな値は、依存値として宣言されなければなりません。エフェクトの依存値のリストは、周囲のコードによって決定されます。
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
が変更されると、エフェクトは新しい値を使用してチャットに再接続します。
リアクティブな値には、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
がリアクティブな値でなくなった(再レンダー時に変更されない)ため、依存配列に入れる必要がなくなりました。エフェクトのコードがリアクティブな値を使用していない場合、その依存配列は空 ([]
) であるべきです。
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
// ...
}
空の依存配列で定義したエフェクトは、コンポーネントの props や state が変更された場合でも再実行されません。
例 1/3: 依存配列を渡す
依存配列を指定すると、エフェクトは最初のレンダー後および依存配列が変わった後の再レンダー後に実行されます。
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different
以下の例では、serverUrl
と roomId
は リアクティブな値であるため、両方とも依存配列の中で指定する必要があります。その結果、ドロップダウンで別のルームを選択したり、サーバ URL の入力欄を編集したりすると、チャットが再接続されます。ただし、message
はエフェクトで使用されていない(依存する値ではない)ため、メッセージを編集してもチャットが再接続されることはありません。
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}/>} </> ); }
エフェクト内で以前の state に基づいて state を更新する
エフェクトから以前の state に基づいて state を更新したい場合、問題が発生するかもしれません。
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
が変更されるたびに、エフェクトがクリーンアップとセットアップを繰り返すことになります。これは望ましくありません。
この問題を解決するには、setCount
に c => c + 1
という state 更新用関数を渡します。
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>; }
c => c + 1
を count + 1
の代わりに渡すようになったので、このエフェクトはもう count
に依存する必要はありません。この修正の結果、count
が変化するたびにインターバルのクリーンアップとセットアップを行わなくてもよくなります。
オブジェクト型の不要な依存値を削除する
エフェクトがレンダー中に作成されたオブジェクトや関数に依存している場合、必要以上にエフェクトが実行されてしまうことがあります。たとえば、このエフェクトは 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
// ...
レンダー中に新たに作成されたオブジェクトを依存値として使用しないでください。代わりに、エフェクトの中でオブジェクトを作成します:
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} /> </> ); }
エフェクトの中で options
オブジェクトを作成するようになったので、エフェクト自体は roomId
文字列にしか依存しません。
この修正により、入力フィールドに文字を入力してもチャットが再接続されることはなくなります。オブジェクトは再レンダーのたびに再作成されるのとは異なり、roomId
のような文字列は別の値に設定しない限り変更されません。依存値の削除に関する詳細を読む。
関数型の不要な依存値を削除する
エフェクトがレンダー中に作成されたオブジェクトや関数に依存している場合、必要以上にエフェクトが実行されてしまうことがあります。たとえば、このエフェクトは 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
// ...
再レンダーのたびに新しい関数を作成すること、それ自体には問題はなく、最適化しようとする必要はありません。ただし、エフェクトの依存値としてそれを使用する場合、毎回のレンダー後にエフェクトが再実行されてしまうことになります。
レンダー中に作成された関数を依存値として使用することは避けてください。代わりに、エフェクトの内部で宣言するようにします。
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} /> </> ); }
createOptions
関数をエフェクト内で定義するようにしたので、エフェクト自体は roomId
文字列にのみ依存することになります。この修正により、入力欄に入力してもチャットが再接続されなくなります。再レンダー時に再作成される関数とは異なり、roomId
のような文字列は他の値に設定しない限り変更されません。依存値の削除について詳しくはこちら。
エフェクトから最新の props と state を読み取る
デフォルトでは、エフェクトからリアクティブな値を読み取るときは、それを依存値として追加する必要があります。これにより、エフェクトはその値の変更に対して「反応」することが保証されます。ほとんどの依存値については、それが望む挙動です。
ただし、時には「反応」をせずに最新の props や state を エフェクト内から読み取りたいことがあるでしょう。例えば、ショッピングカート内のアイテム数をページ訪問ごとに記録する場合を想像してみてください。
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}
url
の変更ごとに新しいページ訪問を記録したいが、shoppingCart
の変更のみでは記録したくない場合はどうすればいいのでしょうか? リアクティブルールに反することなく shoppingCart
を依存配列から除外することはできません。しかし、エフェクト内から呼ばれるコードの一部であるにもかかわらず、そのコードが変更に「反応」しないことを示すことができます。useEffectEvent
フックを使用して、エフェクトイベント (effect event) を宣言し、shoppingCart
を読み取るコードをその内部に移動してください。
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
エフェクトイベントはリアクティブではないため、あなたのエフェクトの依存配列からは常に除く必要があります。これにより、非リアクティブなコード(最新の props や state の値を読むことができるコード)をエフェクトイベント内に入れることができます。onVisit
の中で shoppingCart
を読むことで、shoppingCart
がエフェクトを再実行することがなくなります。
エフェクトイベントがリアクティブなコードと非リアクティブなコードをどのように分離するか詳しく読む。
サーバとクライアントで異なるコンテンツを表示する
お使いのアプリがサーバレンダリングを(直接ないしフレームワーク経由で)使用している場合、コンポーネントは 2 種類の環境でレンダーされます。サーバ上では、初期 HTML を生成するためにレンダーされます。クライアント上では、React がその HTML にイベントハンドラをアタッチするために再度レンダーコードを実行します。これが、ハイドレーションが動作するためには初回レンダーの出力がクライアントとサーバの両方で同一でなければならない理由です。
まれに、クライアント側で異なるコンテンツを表示する必要がある場合があります。たとえば、アプリが localStorage
からデータを読み込む場合、サーバ上ではそれを行うことができません。これは以下の方法で実装できます。
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}
アプリがロードされている間、ユーザは初期レンダーの出力を表示します。ロードとハイドレーションが完了したら、エフェクトが実行され、didMount
が true
にセットされ、再レンダーがトリガされます。これにより、クライアント専用のレンダー出力に切り替わります。エフェクトはサーバ上では実行されないため、初回サーバレンダー時には didMount
は false
のままになります。
このパターンは節度を持って使用してください。遅い接続のユーザは初期コンテンツをかなり長い時間、場合によっては数秒以上表示することになります。なのでコンポーネントの見た目に違和感を与える変更をしないようにしてください。多くの場合、CSS で条件付きに異なるものを表示することで、このようなことはしなくてよくなります。
トラブルシューティング
コンポーネントのマウント時にエフェクトが 2 回実行される
Strict Mode がオンの場合、開発時に React は実際のセットアップの前に、セットアップとクリーンアップをもう一度実行します。
これは、エフェクトのロジックが正しく実装されていることを確認するためのストレステストです。これが目に見える問題を引き起こす場合、クリーンアップ関数に一部のロジックが欠けています。クリーンアップ関数は、セットアップ関数が行っていたことを停止ないし元に戻す必要があります。基本原則は、ユーザがセットアップが一度呼ばれた場合(本番環境の場合)と、セットアップ → クリーンアップ → セットアップというシーケンスで呼ばれた場合(開発環境の場合)で、違いを見分けられてはいけない、ということです。
どのようにバグを見つけるのに役立つか と、ロジックを修正する方法 について詳しく読む。
エフェクトが再レンダーごとに実行される
まず、依存配列の指定を忘れていないか確認してください。
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!
依存配列を指定しているにもかかわらず、エフェクトがループで再実行される場合、それは再レンダーごとに依存する値のどれかが変わっているためです。
この問題は、手動で依存する値をコンソールにログ出力することでデバッグできます。
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
次に、コンソール上の異なる再レンダーから表示された配列を右クリックし、それぞれで “Store as a global variable” を選択します。最初のものが 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
でラップしてください。
エフェクトが無限ループで再実行され続ける
エフェクトが無限ループで実行される場合、以下の 2 つの条件が成立しているはずです。
- エフェクトが何らかの state を更新している。
- その state 更新により再レンダーが発生し、それによりエフェクトの依存配列が変更されている。
問題を修正する前に、エフェクトが外部システム(DOM、ネットワーク、サードパーティのウィジェットなど)に接続しているかどうかを確認してください。エフェクトが state を設定する必要がある理由は何ですか? 外部システムと同期するためですか? それとも、アプリケーションのデータフローをそれで管理しようとしているのでしょうか?
外部システムがない場合、そもそもエフェクトを削除することでロジックが簡略化されるかどうか、検討してください。
もし本当に外部システムと同期している場合は、エフェクトがいつ、どのような条件下で state を更新する必要があるか考えてみてください。何か、コンポーネントの視覚的な出力に影響を与える変更があるのでしょうか? レンダーに使用されないデータを管理する必要がある場合は、ref(再レンダーをトリガしない)の方が適切かもしれません。エフェクトが必要以上に state を更新(して再レンダーをトリガ)していないことを確認してください。
最後に、エフェクトが適切なタイミングで state を更新しているものの、それでも無限ループが残っている場合は、その state の更新によりエフェクトの依存配列のどれかが変更されているためです。依存配列の変更をデバッグする方法を確認してください。
コンポーネントがアンマウントされていないのにクリーンアップロジックが実行される
クリーンアップ関数は、アンマウント時だけでなく、依存配列が変更された後の再レンダー後にも実行されます。また、開発中には、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]);
エフェクトのライフサイクルがコンポーネントのライフサイクルとどのように異なるかを学びましょう。
エフェクトが表示に関することを行っており、実行前にちらつきが見られる
エフェクトがブラウザの画面描画をブロックする必要がある場合は、useEffect
の代わりに useLayoutEffect
を使用してください。ただし、これはほとんどのエフェクトには必要ないということに注意してください。これは、ブラウザ描画の前にエフェクトを実行することが重要な場合にのみ必要です。例えば、ユーザがツールチップを見る前に、ツールチップのサイズを測定して配置するために使用します。