useEffectの基本!副作用処理の意味と役割をやさしく解説
生徒
「先生、useEffectってよく聞くんですけど、何に使うんですか?」
先生
「useEffectは副作用と呼ばれる処理を扱うためのReactのフックです。データの取得や外部操作を安全に行うために使います。」
生徒
「副作用ってどんなものですか?難しそうです」
先生
「例えばネットからデータを取ってきたり、ブラウザのタイトルを変えたり、イベントリスナーを追加したりする処理が副作用です。画面の描画以外で何か外側に影響を与える処理だと覚えてください。」
生徒
「副作用は危険なものなんですか?」
先生
「危険というより『正しく管理しないと予期せぬ動作や性能問題が起きることがある』と考えてください。useEffectはその管理を簡単にしてくれる道具です。」
1. 副作用(サイドエフェクト)とは何か?
副作用とは、画面に表示すること以外の処理で、外部とやり取りするものを指します。具体的にはデータの取得、ファイル読み書き、タイマーやインターバル、ブラウザのタイトル変更、イベントリスナーの登録、サードパーティのライブラリ初期化などが該当します。
初めて学ぶ方にはわかりにくい概念ですが、身近な例で言うと「メールを送る」「データベースに保存する」「外部サービスに問い合わせる」といった画面外で何かをする処理が副作用です。画面表示は副作用ではなく純粋な出力であると考えてください。
副作用は順序やタイミングが重要です。コンポーネントが表示されたタイミングや消えたタイミングで副作用を始めたり終えたりする必要があるため、その管理にuseEffectが役立ちます。
2. useEffectの基本的な書き方と仕組み
useEffectは次のように書きます。引数に実行したい関数を渡し、その関数内で副作用の処理を行います。必要ならクリーンアップ用の関数を返します。
useEffect(() => {
// 副作用の処理(データ取得やイベント登録など)
return () => {
// クリーンアップ処理(イベント解除やタイマー停止など)
};
}, [依存する値]);
依存配列には、その副作用が再実行されるきっかけとなる値を列挙します。依存配列を空にすると「最初の一回だけ実行」されます。依存配列を省略すると毎回レンダーの後に実行されます。どれを使うかは処理の目的によって決めます。
内部的にはReactは依存配列の中身を比較し、変化があれば副作用を再実行します。そのため不要な実行を避けるために依存配列を正しく指定することが大切です。
3. 簡単な例:ブラウザタイトルを更新する
まずはわかりやすい例で、カウントに応じてブラウザのタブタイトルを更新するコードを見ていきます。これは副作用の中でも副作用自体が軽く、安全に扱える典型的な例です。
import React, { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `カウント: ${count}`;
}, [count]);
return (
<div>
<p>現在のカウント: {count}</p>
<button onClick={() => setCount(count + 1)}>カウントアップ</button>
</div>
);
}
export default App;
この例では依存配列にcountを指定しているため、countが変化したときだけ処理が実行されます。もし依存配列を空にすると最初の一回だけ実行され、countが変わってもタイトルは更新されません。どのタイミングで処理させたいかを考えて依存配列を選びましょう。
4. 実用例:データを取得する(非同期処理)の書き方
APIからデータを取得する処理は典型的な副作用です。非同期処理を行うときは、コンポーネントがアンマウントされた後に状態を更新しないように気を付ける必要があります。次の例ではキャンセルフラグを使ってその問題を避けています。
import React, { useState, useEffect } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
let cancelled = false;
fetch("https://jsonplaceholder.typicode.com/users")
.then(res => res.json())
.then(data => {
if (!cancelled) setUsers(data);
})
.catch(err => {
console.error(err);
});
return () => {
cancelled = true;
};
}, []);
return (
<div>
<h2>ユーザー一覧</h2>
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
</div>
);
}
export default Users;
実運用ではfetchの代わりにAxiosなどのライブラリを使ったり、AbortControllerでフェッチ自体を中止したりします。いずれにしても非同期処理が完了する前にコンポーネントが消える可能性を考慮してコーディングすることが重要です。
5. クリーンアップ処理を忘れない
useEffect内でイベントリスナーやタイマーを登録した場合、必ずクリーンアップ用の関数で解除してください。解除を忘れるとメモリやイベントが蓄積され、アプリの挙動が悪くなります。下はウィンドウリサイズのイベントを登録し、解除する例です。
useEffect(() => {
const handler = () => console.log("リサイズされました");
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
}, []);
クリーンアップはコンポーネントがアンマウントされるときや次の副作用が実行される前に呼ばれます。複数の副作用をまとめすぎず、目的ごとに分けて書くとクリーンアップも分かりやすくなります。
6. 依存配列の扱い方と落とし穴
依存配列を正しく理解することはuseEffectを使いこなす上で最も重要なポイントの一つです。依存配列には副作用の中で参照する変数や関数をすべて入れる必要があります。入れ忘れると古い値に基づいて処理が行われ、バグの原因になります。
ただしオブジェクトや配列、関数などの参照型の値を依存に含めると、それらが毎回新しい参照になることで副作用が頻繁に再実行されることがあります。その場合はuseCallbackやuseMemoで参照の安定化を検討するか、依存を分割して設計を見直しましょう。
また、依存配列に関数を入れるときは、その関数自体が外部の状態に依存している可能性があるため、依存のルールを整理することが重要です。ESLintのreact-hooksプラグインを導入すると依存漏れを検出してくれるので便利です。
7. 実務でのベストプラクティスとデバッグのコツ
- 副作用は必要なタイミングだけ実行するよう依存配列を適切に指定する。
- 非同期処理ではエラーハンドリングとキャンセル処理を忘れない。
- イベントやタイマーはクリーンアップで必ず解除する。
- 複雑な副作用はカスタムフックに切り出して再利用性を高める。
デバッグするときはコンソールログで副作用の実行タイミングを確認したり、Reactの開発者ツールでコンポーネントのレンダー回数をチェックしたりすると原因がつかめます。レンダー回数が多すぎる場合は依存配列や親コンポーネントの設計を見直しましょう。
8. 用語解説(初心者向け)
副作用(サイドエフェクト): 画面描画以外の外部に影響を与える処理。
クリーンアップ: 副作用で登録したものを解除する処理。アンマウント時に実行される。
依存配列: useEffectの第二引数。副作用を再実行する条件を指定する配列。
9. クラスコンポーネントとの対応関係
従来のクラスコンポーネントではライフサイクルメソッドとしてcomponentDidMount、componentDidUpdate、componentWillUnmountなどを使って副作用を管理していました。useEffectはこれらを関数コンポーネントの中でまとめて表現できる仕組みです。
例えば「コンポーネントが最初に表示されたときだけ処理をする」はcomponentDidMountに相当し、依存配列を空にしたuseEffectで実現します。一方「ある値が更新されたときに処理をする」はcomponentDidUpdateの役割をuseEffectで実現します。
10. よくあるエラーとその対処法
useEffectを使っているときに出やすいエラーと対処法をまとめます。
- 「Can't perform a React state update on an unmounted component」:非同期処理の完了後に状態更新している可能性があります。キャンセル処理やフラグで回避しましょう。
- 無限ループで副作用が何度も実行される:依存配列に毎回変わる参照型の値が入っていないか確認します。関数やオブジェクトを依存にする場合は参照が安定するように工夫します。
- 意図したタイミングで実行されない:依存配列に漏れがないか、あるいは不要に値を入れていないかを見直します。ESLintのreact-hooksルールを有効にすると依存漏れを検出してくれます。
11. 実践ワンポイント例:連続クリックで正しく増やす
連続で状態を更新する際に古い状態に基づいて更新してしまうバグを防ぐ方法です。setStateに関数を渡すと最新の状態を使って更新できます。
import React, { useState } from "react";
function FastCounter() {
const [count, setCount] = useState(0);
const handleMulti = () => {
// 間違った書き方: 連続クリックで正しく動かないことがある
// setCount(count + 1);
// setCount(count + 1);
// 正しい書き方: 関数形式で最新値を使う
setCount(c => c + 1);
setCount(c => c + 1);
};
return (
<div>
<p>カウント: {count}</p>
<button onClick={handleMulti}>二回増やす</button>
</div>
);
}
export default FastCounter;
このように、状態更新関数に関数を渡すと常に最新の値を使って計算されます。副作用と組み合わせる場合も同様の考え方が必要です。
12. 最終チェックリスト
最後にuseEffectを扱うときに覚えておきたい簡単なチェックリストです。依存配列を確認する、クリーンアップを忘れない、非同期処理のキャンセルを検討する、複雑ならカスタムフックに分ける、これらを習慣にすると安全に副作用を扱えます。
まとめ
useEffectの仕組みをあらためて振り返ると、Reactの中でもとくに理解しておく必要がある重要なテーマであることがわかります。画面に表示される部分だけではなく、データ取得やイベント管理のような外部とのやり取りを安全に扱うための役割を担っているため、正しく使えるようになるとアプリ全体の完成度が大きく向上します。副作用はタイミングや依存する値によって結果が変わるため、依存配列を意識しながら副作用を整理し、それぞれの処理をコンポーネントのライフサイクルに沿ってコントロールすることが重要です。依存配列の記述が適切であれば、不要な再実行を避けて性能を保てるだけではなく、誤ったタイミングで動作してしまう混乱を防ぐことができます。
また、副作用を適切にクリーンアップできるかどうかは、Reactの動作を安定させるための大事な要素です。イベントリスナーを解除し忘れると複数回の実行で処理が積み上がり、気づかないうちにアプリが重くなることがあります。同様に、タイマーやAPI通信もクリーンアップが必要な場面が多くあります。とくに非同期処理ではコンポーネントが消えた後に結果が返ってくることがあり、そのまま状態を更新するとエラーにつながるため、キャンセル処理を組み合わせた記述を習得しておくと安心です。useEffectのクリーンアップは、こうしたトラブルを未然に防ぐための基本的で大切な考え方です。
さらに、依存配列に何を含めるかによって副作用の動きが大きく変わる点も重要です。依存を省略した場合は毎回実行されてしまい、逆に必要な値を含めないと古い値のまま処理が行われてしまいます。依存関係を整理し、参照型の値を扱うときにはuseCallbackやuseMemoの利用も検討することで、より安定した動作が得られます。こうした知識は小さなアプリでも役に立ち、大きなアプリではさらに大きな効果を発揮します。
下記では今回学んだ内容を応用し、依存配列の違いによってどのような挙動になるのかを確認できるサンプルプログラムを用意しました。副作用が実行されるタイミングを感覚的に理解できるため、実際に動かしながら振り返るのに役立ちます。
サンプルプログラム:依存配列の違いを確認する
import React, { useState, useEffect } from "react";
function EffectDemo() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// 毎回実行されるeffect
useEffect(() => {
console.log("毎回実行される副作用");
});
// 初回のみ実行されるeffect
useEffect(() => {
console.log("初回のみ実行される副作用");
}, []);
// countが変わると実行
useEffect(() => {
console.log("countが変化したときの副作用");
}, [count]);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="入力すると毎回effectが動作"
/>
</div>
);
}
export default EffectDemo;
このサンプルでは依存配列に何を入れるかによって、副作用がどのタイミングで動くのかが明確に分かるようになっています。「毎回実行」「初回だけ実行」「特定の値が変わったときに実行」のように、それぞれの副作用を正しく管理するための基本的な仕組みを確認することができます。とくに、入力欄を操作するたびに実行される副作用と、カウントが変わったときにだけ実行される副作用を比較することで、依存配列の重要性が実感できるでしょう。
useEffectは副作用の管理をわかりやすくし、コンポーネントがどのタイミングで何を行うべきかを整えるための道具です。その理解が深まれば、ネットワーク通信やイベント処理、外部ライブラリとの連携など、実際の開発で頻繁に必要となる場面にうまく対応できるようになります。副作用をどのように組み合わせてアプリ全体の流れをつくるかは、Reactを扱ううえでの大きな学びです。このまとめが、より安定したアプリケーション開発に役立つ指針となるはずです。
生徒:「useEffectがどんなときに必要なのか、やっとつながりました。画面の外で行う処理をまとめるためなんですね。」
先生:「その通りです。画面更新とは別の動きを整理するための仕組みなので、副作用が多い場面ほど力を発揮します。」
生徒:「依存配列の違いで実行タイミングが変わるのも面白いです。間違えると動きが変になる理由もよくわかりました。」
先生:「依存配列はuseEffectの肝ですね。適切に書けると性能も安定しますし、予期せぬ挙動も防げます。」
生徒:「クリーンアップ処理の大事さもよく理解できました。イベントを解除しないと危険なんですね。」
先生:「そのとおり。解除しないと積み重なって動作が重くなります。useEffectでは『始める』だけでなく『終わらせる』ことも大切です。」
生徒:「今回のサンプルも参考になりました。いろいろ試して動作を確かめてみます!」
先生:「ぜひ触ってみてください。副作用の扱いに慣れるとReactの理解がぐっと深まりますよ。」