【React】useEffectの基本:副作用処理を理解する

JavaScript

こんにちは、かつコーチです。

前回はuseStateでオブジェクトや配列を安全に更新する方法を解説しました。

今回は、Reactの中でも特につまずきやすいと言われるuseEffectを扱います。

「なんとなく動いているけど、実は理解できていない」という人が多いフックなので、ここで基礎からしっかり整理しておきましょう。

副作用処理とは?

レンダリングと副作用の違い

Reactのコンポーネントは、基本的に「propsやstateを受け取って、画面(JSX)を返す」という純粋な関数として考えます。

しかし実際のアプリでは、それだけでは完結しない処理がたくさんあります。

  • APIからデータを取得する
  • タイマーをセットする
  • ブラウザのイベントを購読する
  • ドキュメントのタイトルを書き換える

これらは「画面を返す」こととは別の、コンポーネントの外の世界とやり取りする処理です。

Reactではこれを副作用(side effect)と呼び、useEffectというフックを使って扱います。

なぜレンダリング処理の中に直接書けないのか

「副作用処理も普通に関数の中に書けばいいのでは?」と思うかもしれません。

しかし、レンダリング処理の中に直接副作用を書いてしまうと、コンポーネントが再レンダリングされるたびに毎回その処理が実行されてしまいます。

たとえばAPI通信をレンダリング処理の中に直接書くと、画面が再描画されるたびに無限にAPIを呼び続ける、という事故につながりかねません。

useEffectは「レンダリングが終わった後に、指定したタイミングだけ実行する」という制御を与えてくれる仕組みです。

基本の書き方

useEffectの基本形

import { useEffect, useState } from "react";

function DocumentTitleUpdater() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `クリック回数: ${count}`;
  });

  return (
    <button onClick={() => setCount(count + 1)}>クリックする({count}回)</button>
  );
}

export default DocumentTitleUpdater;

useEffectは第1引数に「実行したい処理」を関数として渡します。

このコードでは、レンダリングされるたびにブラウザのタブタイトルを更新しています。

依存配列で実行タイミングを制御する

useEffectの第2引数には依存配列を渡せます。

import { useEffect, useState } from "react";

function WelcomeMessage() {
  const [name, setName] = useState("ゲスト");

  useEffect(() => {
    console.log(`${name}さんがページを開きました`);
  }, []); // 空配列 → 初回レンダリング時のみ実行

  return <p>ようこそ、{name}さん</p>;
}

export default WelcomeMessage;

依存配列に何を渡すかによって、useEffectの挙動が大きく変わります。

依存配列の書き方実行タイミング
省略毎回のレンダリング後に実行
[](空配列)初回レンダリング後の1回だけ実行
[value]初回に加え、valueが変化するたびに実行

初心者のうちは「とりあえず[]をつけておく」と覚えている人もいますが、これは正確には「依存する値が本当にない場合だけ[]を使う」というのが正しい理解です。

APIからデータを取得する実例

useEffectが最もよく使われる場面のひとつが、コンポーネント表示時のデータ取得です。

import { useEffect, useState } from "react";

type User = {
  id: number;
  name: string;
};

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    let isMounted = true;

    fetch(`https://example.com/api/users/${userId}`)
      .then((res) => res.json())
      .then((data: User) => {
        if (isMounted) {
          setUser(data);
        }
      });

    return () => {
      isMounted = false;
    };
  }, [userId]);

  if (!user) return <p>読み込み中...</p>;

  return <p>{user.name}さんのプロフィール</p>;
}

export default UserProfile;

userIdが変わるたびにデータを再取得したいので、依存配列には[userId]を指定しています。

よくあるつまずきポイント・エラー対処

つまずき:依存配列を省略して無限ループになる

私がuseEffectを初めて使ったとき、依存配列そのものを丸ごと書き忘れて、画面がフリーズしたことがあります。

❌ Before:依存配列がなく、更新のたびに再実行されてしまう

import { useEffect, useState } from "react";

function BrokenCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // stateを更新 → 再レンダリング → useEffectが再実行 → 以下ループ
  });

  return <p>{count}</p>;
}

export default BrokenCounter;

このコードは、useEffectの中でsetCountを呼んでいるにもかかわらず、依存配列を指定していません。

そのため「レンダリング→useEffect実行→state更新→再レンダリング→useEffect再実行……」という無限ループに陥ります。

私はこれを開発中のブラウザで実行してしまい、タブが固まってCPU使用率が跳ね上がる、という状態を実際に経験しました。

慌てて開発者ツールを開き、原因を1行ずつ追ってようやく気づいたのを覚えています。

✅ After:空の依存配列で初回のみ実行する

import { useEffect, useState } from "react";

function FixedCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount((prev) => prev + 1); // 初回レンダリング時のみ実行される
  }, []);

  return <p>{count}</p>;
}

export default FixedCounter;

useEffectの中でstateを更新するときは、「この処理が何をきっかけに再実行されるのか」を依存配列で必ず明示するようにしましょう。

応用・一歩先の使い方

クリーンアップ関数で後片付けをする

useEffectの中で返す関数はクリーンアップ関数と呼ばれ、コンポーネントが画面から消えるとき(アンマウント時)や、次のuseEffectが実行される直前に呼び出されます。

import { useEffect, useState } from "react";

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const timerId = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    return () => {
      clearInterval(timerId); // コンポーネントが消えるときにタイマーを止める
    };
  }, []);

  return <p>経過時間: {seconds}秒</p>;
}

export default Timer;

setIntervalのようなタイマーや、イベントリスナーの登録は、クリーンアップ関数で必ず後片付けをしないと、コンポーネントが画面から消えた後も裏で動き続けてしまいます。

これはメモリリークの典型的な原因になるので、「登録したら、必ず解除するコードもセットで書く」という意識を持っておきましょう。

依存配列の細かい注意点や、より実践的なハマりどころについては、次回さらに詳しく掘り下げます。

まとめ

この記事のポイント

  • useEffectは、API通信やタイマーなど「コンポーネントの外の世界とやり取りする処理(副作用)」を扱うフック
  • 依存配列を省略すると毎回、[]にすると初回のみ、[value]にするとvalue変化時のみ実行される
  • useEffect内でstateを更新する際は、依存配列を正しく設定しないと無限ループになりうる
  • クリーンアップ関数(返り値の関数)で、タイマーやイベントリスナーの後片付けを行う
  • API通信を行うuseEffectでは、コンポーネントがアンマウントされた後の処理にも配慮する

次に読むべき記事

次回は、useEffectの依存配列でつまずきやすいポイントを、実践的なケースを交えてさらに詳しく解説します。

今回の基本を踏まえて、より実務に近い落とし穴を確認していきましょう。

→ 次の記事:useEffectの依存配列でハマらないためのコツ

タイトルとURLをコピーしました