useEffect
の役割
useEffect
は、「コンポーネントのレンダリング」と「副作用の実行」を分離するためのフック です。
主に以下の目的で使用されます。
** useEffect
の目的**
-
副作用の適切な実行タイミングを制御する
- 初回マウント時のみ実行(
[]
依存配列) - 依存関係の変更時に実行(
[dependency]
) - クリーンアップ処理を適用(
return
でリソース解放)
- 初回マウント時のみ実行(
-
不要な副作用の実行を防ぐ
- 過剰な再レンダリングを避ける(依存配列の適切な管理)
- 不要なデータ取得を抑制(前回のリクエストをキャンセルするなど)
副作用(Side Effects)とは?
「副作用(Side Effect)」とは、コンポーネントの関数実行(レンダリング)とは直接関係がない処理 を指します。
副作用は、コンポーネント外の状態を変更する操作 を含むため、純粋関数では扱えません。
** 副作用の具体例**
- データ取得(API コール)
- イベントリスナーの登録・解除
- 外部の状態(
localStorage
/sessionStorage
など)を変更 - DOM の直接操作
これらの副作用を適切に管理するために、useEffect
を活用し、必要なタイミングで実行・クリーンアップを行うことが重要 です。
なぜ副作用の管理には useEffect
が用いられるのか?
① レンダリングを純粋な UI 計算処理として保ち、副作用を分離する
なぜコンポーネントのレンダリングを純粋な UI 計算処理として保つことが重要なのか?
-
副作用の実行が必要なタイミングは、レンダリングのタイミングとは必ずしも一致しない
-
データ取得は、コンポーネントが描画されるたびに実行する必要はなく、初回マウント時のみで十分なケースが多い。
-
例えば、API からデータを取得する場合:
useEffect(() => { fetch("/api/posts") .then((res) => res.json()) .then((data) => setPosts(data)); }, []); // 空の依存配列を指定することで、初回マウント時のみ実
-
もし
useEffect
を使わず、レンダリング時に直接データを取得すると、レンダリングごとに API リクエストが発生し、無駄な負荷がかかる。
-
-
不要な再レンダリングを防ぐ
-
データ取得の結果を
setState
で保存する場合、そのstate
の変更が 再レンダリングを引き起こす。 -
もしデータ取得がレンダリングのたびに行われると、無限ループのように再レンダリングが繰り返されてしまう。
const [posts, setPosts] = useState([]); // NG: 直接データ取得を行うと、レンダリングのたびに実行される const data = fetch("/api/posts").then((res) => res.json()); setPosts(data);
-
useEffect
を使えば、依存配列を適切に設定することで 不要な副作用の再実行を防ぐ ことができる。
-
② UI の予測可能性を高める
-
React は「状態 (
state
) と入力 (props
) に基づいて UI を決定する」という原則を持つ。 -
副作用が適切に管理されていないと、状態がどのように変化するか予測が難しくなり、デバッグが困難になる。
-
例えば、コンポーネントのマウント時に API からデータを取得し、そのデータに基づいて
state
を更新する場合:useEffect(() => { fetch("/api/posts") .then((res) => res.json()) .then((data) => setPosts(data)); }, []);
-
useEffect
を使わずにこの処理をコンポーネントの中で直接書いてしまうと、- どのタイミングで API コールが実行されるのか不明確
- データが取得できる前の
state
の状態が分かりづらくなる - 予期せぬ再レンダリングが発生する
-
そのため、
useEffect
に副作用を分離することで、「このコンポーネントはstate
の変更によって UI を更新するだけ」 というシンプルな構造を保つことができる。
③ パフォーマンス最適化のため
-
副作用が適切に管理されないと、レンダリングのたびに不要な処理が実行、関数の再生成が起こり、パフォーマンスが低下する。
-
特に API コールやイベントリスナーの登録 などは、無駄に実行されるとアプリのレスポンスが遅くなる。
-
useEffect
を使えば、以下のように 依存配列を適切に設定することで最適なタイミングで実行可能:useEffect(() => { window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); // クリーンアップ }; }, []); // 初回マウント時のみ登録
-
これにより、不要なイベントリスナーの登録・解除が防がれ、メモリリークのリスクも回避できる。
useEffect
内で setState
を行う場合の React のレンダリングとコミットの流れ(初回マウント時のみ useEffect
実行するケース)
-
初回のレンダリング
- コンポーネントが初回レンダリングされ、仮の UI が描画される。
-
この時点では
useEffect
はまだ実行されない(レンダリング後に実行されるため)。
-
初回コミット
- 初期 UI がブラウザに描画され、DOM に反映される。
-
ここで
useEffect
が実行される。
-
useEffect
内でsetState
が発生-
useEffect
内でsetState
を実行すると、新しい状態が適用され、再レンダリングがトリガーされる。
-
-
再レンダリング
-
setState
によって変更された状態を反映し、React がコンポーネントを再レンダリングする。
-
-
再コミット
- 変更後の UI が DOM に適用される。
-
useEffect
は依存配列[]
のため再度実行されない(初回マウント時のみ実行)。
グローバル状態の更新は副作用に挙げられるか?
Yes、グローバル状態の 「更新」 は副作用とみなされる。
理由
-
「取得」自体も時に副作用である
- グローバル状態(例: Redux / Context API / Zustand など)を取得するだけでは UI の計算に影響を与えない場合が多いが、
「取得時に API からデータをフェッチする」場合は明らかに副作用となる。
- グローバル状態(例: Redux / Context API / Zustand など)を取得するだけでは UI の計算に影響を与えない場合が多いが、
-
「更新」は副作用として管理すべき
-
useEffect
を使わずにsetGlobalState
をコンポーネントのレンダリング時に実行すると、
レンダリングのたびに状態が更新され、不要な再レンダリングが発生する可能性がある。
-
-
レンダリングとグローバル状態の更新を同時に行うと「UI の計算結果がブレる」
- 状態の変更が「いつ適用されるか」 が不明確になり、意図しない UI の不整合が発生するリスクがある。
マウント時にグローバル状態を扱いたいケースでの適切な管理方法
グローバル状態の「取得」 → useSelector()
や useContext()
などを使い、レンダリング時に取得
グローバル状態の「更新」 → useEffect()
+ dispatchや mutation を使って副作用として実行(マウント時にそもそも更新するケースはあまりないかもしれないが、、、)