useEffect は、React の外部にある仕組みとコンポーネントを同期するための Hook です。ブラウザ API、イベント購読、タイマー、WebSocket 接続などを扱うときに使います。
Effect が必要かを先に確かめる
Effect は「レンダーによって発生する副作用」を扱う手段です。しかし、すべての処理を Effect に入れる必要はありません。
- props や state から計算できる値は、レンダー中に計算する
- クリックなどの操作への反応は、イベントハンドラで扱う
- React の外部システムとの同期には、Effect を使う
たとえば、完了済みタスク数を state にコピーして Effect で更新する必要はありません。一方、ブラウザのウィンドウ幅を購読する処理は React の外部との同期なので Effect が適しています。
基本形と依存配列
次の Effect は、コンポーネントが表示された後にリサイズイベントを購読します。
import { useEffect, useState } from 'react';
function WindowWidth() {
const [width, setWidth] = useState(0);
useEffect(() => {
const updateWidth = () => setWidth(window.innerWidth);
updateWidth();
window.addEventListener('resize', updateWidth);
return () => window.removeEventListener('resize', updateWidth);
}, []);
return <p>画面幅: {width}px</p>;
}
第2引数の依存配列には、Effect 内で参照し、コンポーネントの外部から変化しうる値を並べます。[] は依存値がないことを表し、通常は表示後に一度だけ実行されます。依存値が変わる場合は、新しい値で同期し直します。
cleanup で後始末をする
Effect から関数を返すと cleanup になります。cleanup はコンポーネントが画面から外れるときだけでなく、依存値の変更によって Effect を再実行する前にも呼ばれます。
イベントリスナー、タイマー、接続のように開始と終了が対になる処理では、開始した場所で必ず cleanup を返します。
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
開発時には、Effect が setup → cleanup → setup と実行されることがあります。これは cleanup が正しく実装されているかを確かめるためで、cleanup 後に再接続しても正しく動くように書くのが大切です。
非同期処理で古い結果を反映しない
Effect 内で通信する場合、依存値が変わった後に古いリクエストが完了することがあります。cleanup で「この Effect は無効になった」と記録し、古い結果で state を更新しないようにします。
useEffect(() => {
let cancelled = false;
async function loadUser() {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
if (!cancelled) setUser(user);
}
loadUser();
return () => { cancelled = true; };
}, [userId]);
実際のアプリケーションでは、フレームワークのデータ取得機構やキャッシュライブラリを使う選択肢もあります。useEffect は外部同期の基本を理解するための重要な API ですが、データ取得のすべてを単独で担うものではありません。
依存配列は実装の一覧ではなく同期条件
依存配列は、Effect が「どの値が変わったら再同期すべきか」を表します。依存配列を空にして警告を抑えるのではなく、Effect が参照する reactive な値を正しく列挙します。
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
roomId を依存配列から外すと、部屋を切り替えても古い接続が残ります。逆に、レンダーごとに新しく作られるオブジェクトや関数を依存に含めると、不要に再同期することがあります。その場合は値を Effect の内側で作る、または設計を分けることを検討します。
UI を見せる前に処理が必要な場合
通常の Effect は React が画面を反映した後に実行されます。そのため、位置計測など、表示前に行わないとちらつきが出る視覚的な処理には useLayoutEffect が必要になることがあります。ただし描画を待たせるため、まずは通常の useEffect で問題がないかを確認します。
通信を中断する方法
cancelled フラグに加え、AbortController で不要になったリクエスト自体を中断できます。
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((response) => response.json())
.then(setUser)
.catch((error) => {
if (error.name !== 'AbortError') throw error;
});
return () => controller.abort();
}, [userId]);
エラー表示、再試行、キャッシュ、サーバーでのデータ取得まで必要になると、手作業の Effect はすぐ複雑になります。その段階では採用しているフレームワークのデータ取得機能や、責務を閉じ込めたカスタム Hook を選ぶのが実践的です。
Effect を減らすチェック
Effect を書く前に、次を確認します。
- 現在の props と state から計算できないか
- ユーザー操作のハンドラに置けないか
- 親から渡された値をそのまま使えないか
- 本当に外部システムとの同期なのか
Effect を少なく保つほど、実行順や依存値を追う負担も小さくなります。