第2回

useEffectの基本:副作用とは何か・データフェッチの実装

ReactのuseEffectを基礎から解説します。副作用の概念・基本構文・マウント時にデータを取得する方法・非同期処理の書き方まで、実務で使えるパターンを実例付きで学べます。

·13分で読める
たける
たける `useEffect` って何のためにあるんですか? `fetch` してデータを取って表示するだけなら、コンポーネントの中に直接書けばいいじゃないですか。
りこ
りこ コンポーネントの本来の仕事は「propsとstateからJSXを返すこと」だけ。データ取得はその外側の仕事──副作用──として分離しないと、毎回レンダリングのたびにリクエストが飛ぶ。
たける
たける あ、実際にコンポーネント内に直接書いたら無限にリクエストが飛んでコンソールが真っ赤になりました。
りこ
りこ 典型的な失敗パターン。fetchしてstateを更新→再レンダリング→またfetch→無限ループ。useEffectはその実行タイミングをReactが制御する。

副作用(side effect)とは

Reactコンポーネントの本来の仕事は「propsとstateからJSXを返す」ことです。それ以外の処理——データの取得・DOMの操作・タイマーのセット・外部サービスへの接続——を**副作用(side effect)**と呼びます。

副作用の例
├── fetch でAPIからデータを取得する
├── localStorage に値を保存・読み込む
├── setInterval / setTimeout を使う
├── イベントリスナーを追加・削除する
└── document.title を変更する

useEffect は、これらの副作用をレンダリングと切り離して安全に実行するための Hook です。

useEffectの基本構文

useEffect(() => {
  // 副作用の処理
}, [依存配列]);

依存配列の書き方によって実行タイミングが変わります。

書き方 実行タイミング
useEffect(fn) 毎回のレンダリング後
useEffect(fn, []) マウント時に1回だけ
useEffect(fn, [a, b]) ab が変わったとき

マウント時に1回だけ実行

function PageTitle({ title }: { title: string }) {
  useEffect(() => {
    document.title = title;
  }, []);  // マウント時に1回だけ

  return <h1>{title}</h1>;
}

データフェッチの実装

useEffectで非同期処理を書くときのベストパターンです。

type Article = {
  id: number;
  title: string;
  content: string;
};

function ArticleDetail({ articleId }: { articleId: number }) {
  const [article, setArticle] = useState<Article | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    // useEffect内のコールバックはasyncにできないため、
    // 内部に非同期関数を定義して呼ぶ
    async function fetchArticle() {
      try {
        setIsLoading(true);
        setError(null);

        const response = await fetch(`/api/articles/${articleId}`);
        if (!response.ok) {
          throw new Error(`エラー: ${response.status}`);
        }
        const data: Article = await response.json();
        setArticle(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : '取得に失敗しました');
      } finally {
        setIsLoading(false);
      }
    }

    fetchArticle();
  }, [articleId]);  // articleIdが変わるたびに再取得

  if (isLoading) return <div>読み込み中...</div>;
  if (error)    return <div>エラー: {error}</div>;
  if (!article) return null;

  return (
    <article>
      <h1>{article.title}</h1>
      <p>{article.content}</p>
    </article>
  );
}

なぜ useEffect の中に async を直接書けないか

// ❌ これはエラーになる
useEffect(async () => {
  const data = await fetch('/api/data');
  // ...
}, []);

useEffect のコールバックは、クリーンアップ関数(または undefined)を返すことが期待されています。async 関数は必ず Promise を返すため、クリーンアップ関数として扱えません。

代わりに、useEffect の内部で非同期関数を定義して呼び出すパターンを使います。

document.title の更新

function useTitleEffect(title: string) {
  useEffect(() => {
    const originalTitle = document.title;
    document.title = `${title} | 生成AI時代のReact実践入門`;

    // アンマウント時に元のタイトルに戻す
    return () => {
      document.title = originalTitle;
    };
  }, [title]);
}

function ArticlePage({ title }: { title: string }) {
  useTitleEffect(title);
  return <article>...</article>;
}

localStorageと連携

function usePersistentState<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);  // value が変わるたびに保存

  return [value, setValue] as const;
}

// 使い方
function Settings() {
  const [theme, setTheme] = usePersistentState('theme', 'light');

  return (
    <select value={theme} onChange={(e) => setTheme(e.target.value)}>
      <option value="light">ライト</option>
      <option value="dark">ダーク</option>
    </select>
  );
}

依存配列の省略はNG

// ❌ 依存配列なし:毎回のレンダリング後に実行される
useEffect(() => {
  document.title = title;
});

// ✅ titleに依存していることを明示
useEffect(() => {
  document.title = title;
}, [title]);

依存配列を省略すると毎回実行され、パフォーマンス問題や無限ループの原因になります。ESLintの exhaustive-deps ルールを使うと依存配列の漏れを自動検出できます。

たける
たける `useEffect(async () => { const data = await fetch(...) })` って書いたら怒られました。なぜ直接asyncにできないんですか?
りこ
りこ useEffectのコールバックはクリーンアップ関数か何も返さない関数でないといけない。asyncにすると暗黙でPromiseを返してしまう。内側に非同期関数を定義して呼ぶ形にする。

まとめ

  • 副作用(データ取得・DOM操作・タイマーなど)は useEffect で扱う
  • useEffect(fn, []) でマウント時に1回だけ実行
  • 非同期処理はuseEffect内で関数を定義して呼び出す
  • useEffect のコールバックを async にしてはいけない
  • 依存配列は省略せず、使っている値を正確に指定する

次の第3回では、依存配列の正しい使い方と、よくある落とし穴を学びます。