useEffect の役割

Part 6 · 副作用 (useEffect)

useEffect の役割

React の外側と同期をとるための道具

useEffect は、React のフックの中でもっとも誤用されているものです。

「何かのタイミングで処理を走らせるもの」と覚えてしまうと、 あらゆる場面で使いたくなり、そのたびにバグを増やします。

正しくは、もっと狭い道具です。React の外側にあるものと、React の状態を合わせるためのもの。 それ以外の用途では、まず使いません。

「React の外側」とは

React が面倒を見てくれるのは、state から画面が作られるところまでです。 それ以外は React の管轄外です。

  • ブラウザのタブ名やアドレス欄
  • タイマー(setInterval など)
  • ブラウザのイベント(スクロール、リサイズ)
  • サーバーとの通信
  • localStorage などの保存領域
  • React で書かれていない他のライブラリ

これらと state を合わせたいときに使うのが useEffect です。

タブ名を state に合わせる

押すとブラウザのタブ名が変わる

0

タブ名は React が管理していません。 JSX に書いて変えられるものではないので、こちらから外へ働きかける必要があります。 これが useEffect の出番です。

title-sync.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";

export function TitleSync() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

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

  useEffect(() => {
    // ブラウザのタブ名は React の管理外。ここで直接書き換える
    document.title = `${count} 回押されました`;

    // このページを離れるときは元に戻す(次の章で詳しく扱う)
    return () => {
      document.title = "React 入門";
    };
  }, [count]); // count が変わったときだけ実行される

  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        押す(タブ名を見て)
      </Button>
    </div>
  );
}

いつ実行されるか

useEffect に渡した処理は、画面が表示されたあとに実行されます。 コンポーネントの関数を実行している最中ではありません。

useEffect(() => {
  // 画面が出たあとに実行される
}, [count]);

第 2 引数の配列は依存配列と呼ばれます。 ここに書いた値が前回と変わっていたときだけ実行されます。

useEffect(fn, [count]); // count が変わったときだけ
useEffect(fn, []);      // 最初の 1 回だけ
useEffect(fn);          // 毎回(ほとんど使わない)

title-sync.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";

export function TitleSync() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

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

  useEffect(() => {
    // ブラウザのタブ名は React の管理外。ここで直接書き換える
    document.title = `${count} 回押されました`;

    // このページを離れるときは元に戻す(次の章で詳しく扱う)
    return () => {
      document.title = "React 入門";
    };
  }, [count]); // count が変わったときだけ実行される

  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        押す(タブ名を見て)
      </Button>
    </div>
  );
}

「〜したとき」ではなく「〜の状態に合わせる」

useEffect を誤用しないためのコツは、言葉の使い方にあります。

// ✕ こう考えると誤用しやすい
「ボタンが押されたときに実行したい」
「count が変わったときに実行したい」

// ○ こう考える
「タブ名を、いまの count に合わせておきたい」

前者はタイミングの話です。 そしてタイミングの話なら、たいていイベントハンドラに書けます。 ボタンが押されたときの処理は onClick の中です。

後者は状態を合わせる話です。 「どういうきっかけであれ、count がこの値なら、外側もこうなっていてほしい」。 この形になっているときだけ、useEffect が正しい選択になります。

title-sync.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";

export function TitleSync() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

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

  useEffect(() => {
    // ブラウザのタブ名は React の管理外。ここで直接書き換える
    document.title = `${count} 回押されました`;

    // このページを離れるときは元に戻す(次の章で詳しく扱う)
    return () => {
      document.title = "React 入門";
    };
  }, [count]); // count が変わったときだけ実行される

  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        押す(タブ名を見て)
      </Button>
    </div>
  );
}

理解できたか確かめる

確認クイズ

useEffect は何のための道具?

確認クイズ

依存配列に書く値を書き忘れるとどうなる?

title-sync.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";

export function TitleSync() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

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

  useEffect(() => {
    // ブラウザのタブ名は React の管理外。ここで直接書き換える
    document.title = `${count} 回押されました`;

    // このページを離れるときは元に戻す(次の章で詳しく扱う)
    return () => {
      document.title = "React 入門";
    };
  }, [count]); // count が変わったときだけ実行される

  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        押す(タブ名を見て)
      </Button>
    </div>
  );
}

この章のまとめ

  • useEffectReact の外側と状態を合わせるための道具
  • 実行されるのは、画面が表示されたあと
  • 依存配列に書いた値が変わったときだけ実行される。 使っている値は全部書く
  • 「〜したとき」ではなく「〜に合わせておきたい」と言えるかで判断する

title-sync.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";

export function TitleSync() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

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

  useEffect(() => {
    // ブラウザのタブ名は React の管理外。ここで直接書き換える
    document.title = `${count} 回押されました`;

    // このページを離れるときは元に戻す(次の章で詳しく扱う)
    return () => {
      document.title = "React 入門";
    };
  }, [count]); // count が変わったときだけ実行される

  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        押す(タブ名を見て)
      </Button>
    </div>
  );
}