データ取得やタイマーの設定、イベントの購読——React コンポーネントには、画面を描くこと以外にもさまざまな処理が必要になる場面があります。そうした「レンダリングの外側で行う処理」をコンポーネントに組み込むためのフックが useEffect です。この記事では、useEffect が何のためにあるのかという基本から、依存配列の使い分け、クリーンアップ関数による後始末、そしてデータ取得やタイマー・リサイズ購読といった実践的な使い方まで、初心者がつまずきやすいポイントを押さえながら解説します。
目次
useEffect は何のためのフックか
useEffect は、コンポーネントに副作用(side effect)を組み込むための React のフックです。副作用とは、画面を描画する(JSX を返す)こと以外の処理を指します。具体的には、サーバーからのデータ取得、setInterval によるタイマー、addEventListener によるイベントの購読、外部ライブラリの初期化や直接的な DOM 操作などがこれにあたります。
これらの処理をコンポーネント本体(関数の中)に直接書くと、レンダリングのたびに実行されてしまい、思わぬ動作や無駄な処理につながります。React では、レンダリング処理はできるだけ「純粋」——同じ入力なら同じ結果を返すだけ——に保つのが原則です。データ取得や購読のような外の世界とやり取りする処理は、レンダリングが済んだ後に useEffect の中で行う、という役割分担になっています。
基本の書き方
useEffect は2つの引数を取ります。第1引数は実行したい処理を書いた関数(副作用の本体)、第2引数は依存配列と呼ばれる配列です。次のような形になります。
import { useEffect } from 'react';
useEffect(() => {
// ここに副作用を書く(レンダリングの後に実行される)
console.log('レンダリングされました');
}, [依存する値]);
第1引数の関数は、コンポーネントが画面に描画された後に実行されます。第2引数の依存配列には、この副作用が「何の値に依存しているか」を並べます。React はこの配列の中身を前回のレンダリング時と見比べ、変化があったときだけ副作用を再実行します。つまり依存配列は、いつ副作用を走らせるかを決めるスイッチの役割を持ちます。
依存配列の3つのパターン
依存配列の書き方によって、副作用が実行されるタイミングが変わります。大きく分けて次の3パターンがあり、これを理解することが useEffect を使いこなす第一歩です。
| 書き方 | 実行されるタイミング | 主な用途 |
|---|---|---|
| 依存配列を省略 | 毎回のレンダリング後に実行 | 基本的に使わない(実行しすぎになりがち) |
[](空配列) | 初回のマウント時に1度だけ実行 | 初回のデータ取得、購読の登録など |
[value](値あり) | 初回と、value が変化するたびに実行 | 特定の値の変化に反応させたいとき |
依存配列を省略すると、レンダリングのたびに毎回副作用が走ります。多くの場面では実行されすぎで、後述する無限ループの原因にもなるため、意図的でない限り避けます。[](空配列)にすると、コンポーネントが最初に表示された1度きりだけ実行されます。[value] のように値を入れると、初回に加えてその値が前回と変わったときにも再実行されます。「この値が変わったら処理をやり直したい」というときに使います。
クリーンアップ関数で後始末をする
useEffect の第1引数の関数は、関数を return することができます。この返した関数をクリーンアップ関数と呼び、副作用の後始末を担当します。具体的には、次に同じ副作用が実行される直前と、コンポーネントが画面から消える(アンマウントされる)ときに呼ばれます。
useEffect(() => {
// 副作用の登録
const id = setInterval(() => {
console.log('1秒ごとに実行');
}, 1000);
// クリーンアップ関数を返す
return () => {
clearInterval(id); // タイマーを解除する後始末
};
}, []);
クリーンアップが必要になるのは、setInterval / setTimeout のタイマー、addEventListener で登録したイベント、WebSocket やデータの購読など、「登録したら明示的に解除しないと残り続ける」種類の副作用です。後始末をしないと、コンポーネントが消えた後もタイマーが動き続けたり、同じイベントリスナーが何重にも登録されたりして、メモリリークや予期しない挙動の原因になります。「登録したら、その解除をクリーンアップで返す」とセットで覚えておくと安全です。
マウント時にデータを取得する
もっとも典型的な使い方が、コンポーネントの表示時にサーバーからデータを取ってくるパターンです。初回に1度だけ実行したいので、依存配列は空配列 [] にします。取得中・取得後の状態は useState で管理します。
import { useEffect, useState } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 初回マウント時に1度だけ実行される
fetch('https://jsonplaceholder.typicode.com/users')
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []); // 空配列なので初回のみ
if (loading) return <p>読み込み中...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default UserList;
依存配列を [] にしているので、このデータ取得はコンポーネントが最初に表示されたときの1回だけ走ります。取得したデータは setUsers で状態に保存し、loading を false に切り替えることで、読み込み中の表示から一覧の表示へと切り替えています。なお、useEffect に渡す関数自体を async にはできないため、上のように内部で fetch(...).then(...) を使うか、async 関数を中で定義して呼び出す形にします。
タイマーとウィンドウのリサイズを扱う
クリーンアップが活きる代表例が、タイマーとイベント購読です。まず、1秒ごとに数字を数え上げるカウンターを見てみましょう。setInterval で登録し、クリーンアップで clearInterval を呼びます。
import { useEffect, useState } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// 前の値をもとに更新(古い値の参照を避ける)
setSeconds((prev) => prev + 1);
}, 1000);
return () => clearInterval(id); // 後始末
}, []);
return <p>経過時間: {seconds} 秒</p>;
}
export default Timer;
ここで setSeconds(prev => prev + 1) と関数型の更新を使っているのがポイントです。もし setSeconds(seconds + 1) と書くと、useEffect が最初に作られた時点の seconds(= 0)をずっと参照し続け、値が 1 から先に進まなくなってしまいます。前の値をもとに更新することで、この「古い値の参照」を避けられます。
次に、ウィンドウの横幅の変化を購読する例です。addEventListener で resize を登録し、クリーンアップで removeEventListener により解除します。
import { useEffect, useState } from 'react';
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// 同じ関数を渡して確実に解除する
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>ウィンドウの幅: {width}px</p>;
}
export default WindowWidth;
addEventListener と removeEventListener には同じ関数(handleResize)を渡す必要があります。無名関数をそれぞれに直接書くと別物として扱われ、解除できません。あらかじめ名前を付けた関数を用意しておくのがコツです。この後始末をしておくことで、コンポーネントが消えた後にリスナーが残り続けるのを防げます。
無限ループになってしまうとき
useEffect でよくあるトラブルが、状態更新が止まらなくなる無限ループです。これは、副作用の中で更新している状態を、依存配列にそのまま入れてしまうときに起きます。
// ❌ count を更新しているのに count に依存している
useEffect(() => {
setCount(count + 1);
}, [count]);
// → count が変わる → 再実行 → また count が変わる … の無限ループ
// ✅ 初回だけ実行したいなら依存配列を空にする
useEffect(() => {
setCount(1);
}, []);
上の悪い例では、副作用が count を更新し、その count の変化がまた副作用を呼び、それがさらに count を更新する……という循環に陥ります。対処は、その副作用が本当に依存すべき値だけを依存配列に入れることです。オブジェクトや配列、関数を毎レンダリングで新しく作って依存配列に渡している場合も、参照が毎回変わるため同じループが起きます。その場合は useMemo や useCallback で参照を安定させるか、依存の設計自体を見直します。
依存配列の指定漏れによる不具合
無限ループとは逆に、依存配列に入れるべき値を書き忘れると、副作用が古い値を参照し続ける不具合が起こります。たとえば副作用の中で props や state を使っているのに、それを依存配列に入れていないと、値が変わっても副作用が更新されず、初回時点の古い値のまま動き続けます。
原則として、副作用の中で使っている値(props・state・その場で定義した関数など)は、すべて依存配列に入れるのが基本です。手動で管理するのは漏れやすいため、eslint-plugin-react-hooks の exhaustive-deps ルールを有効にしておくと、不足している依存を警告してくれます。前述のタイマーのように「更新はしたいが依存には入れたくない」値がある場合は、関数型の更新(prev => ...)を使うことで依存から外せます。
開発モードで副作用が2回実行される
React 18 以降、開発モードで <StrictMode> を使っていると、マウント時に useEffect が意図的に2回実行されます(登録 → クリーンアップ → 再登録)。これはバグではなく、クリーンアップが正しく書けているかを開発中に検出するための仕組みです。本番ビルドでは1回しか実行されません。もし2回実行で表示が二重になるなどの問題が出るなら、それはクリーンアップが不足しているサインなので、後始末を正しく書けば解消します。
まとめ
useEffect は、データ取得・タイマー・イベント購読といった副作用を、レンダリングが済んだ後に実行するためのフックです。useEffect(() => { ... }, [依存]) の形で使い、依存配列を省略すれば毎回、[] なら初回のみ、[value] なら値の変化時に実行されます。タイマーや購読のように後始末が必要な副作用では、関数を return してクリーンアップを書くのが鉄則です。無限ループや古い値の参照といったトラブルの多くは依存配列の指定に原因があるため、exhaustive-deps ルールを頼りに、使っている値を正しく依存配列へ反映させましょう。この基本を押さえれば、外の世界とやり取りする処理を安全にコンポーネントへ組み込めるようになります。