本文へスキップ
その useEffect は要らない

Part 6 · 副作用 (useEffect)

35 / 64

その useEffect は要らない

書きがちなアンチパターンを 4 つ潰す

前の章で「useEffect は React の外側と同期するための道具」と書きました。

裏を返すと、外側が出てこないなら要らないということです。

ここでは、実際によく書かれてしまう形を 6 つ取り上げて、 それぞれどう書けばよかったのかを見ます。

1. 計算できる値を、effect で作っている

いちばん多いのがこれです。

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

useEffect(() => {
  setCount(items.length);
}, [items]);

動きはします。ですが問題があります。

effect で件数を合わせるうまくいかない例

動くが、遠回りしている

件数: 1

りんご

  • 2 回描き直している … items が変わって 1 回、そのあと effect が setCount してもう 1 回
  • 一瞬ずれた状態が表示される … 1 回目の描画では、count はまだ古い値
  • Part 4 の「state は最小限にする」 でやった二重管理そのもの

正解は、ずっと単純です。

ただ計算する直した例

useEffect も state も要らない

件数: 1

りんご

effect-derived.tsx

"use client";

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

/*
  よくある誤用の例。本来 lint に止められる書き方なので、
  「なぜ良くないか」を見せるためにこのファイルだけ黙らせている。
*/
/* eslint-disable react-hooks/set-state-in-effect */

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

  const [items, setItems] = useState(["りんご"]);
  const [count, setCount] = useState(1);

  // items が変わったら count を合わせる、という発想
  useEffect(() => {
    setCount(items.length);
  }, [items]);

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <p>件数: {count}</p>
        <p className="text-muted-foreground">{items.join(", ")}</p>
      </div>

      <Button size="sm" onClick={() => setItems((current) => [...current, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

2. props を state に写している

// ✕ props が変わるたびに state を合わせている
const [name, setName] = useState(props.name);

useEffect(() => {
  setName(props.name);
}, [props.name]);

これも二重管理です。props.name という正解がすでにあるのに、その写しを持ってしまっています。

// ○ そのまま使えばよい
<p>{props.name}</p>

「編集できるようにしたいから state が必要」という場合もありますが、 そのときは編集中の値だけを state にします。 表示用に丸ごと写す必要はありません。

effect-derived.tsx

"use client";

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

/*
  よくある誤用の例。本来 lint に止められる書き方なので、
  「なぜ良くないか」を見せるためにこのファイルだけ黙らせている。
*/
/* eslint-disable react-hooks/set-state-in-effect */

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

  const [items, setItems] = useState(["りんご"]);
  const [count, setCount] = useState(1);

  // items が変わったら count を合わせる、という発想
  useEffect(() => {
    setCount(items.length);
  }, [items]);

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <p>件数: {count}</p>
        <p className="text-muted-foreground">{items.join(", ")}</p>
      </div>

      <Button size="sm" onClick={() => setItems((current) => [...current, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

3. イベントで済むことを effect でやっている

// ✕ 送信されたことを state にして、effect で検知している
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
  if (submitted) {
    sendData();
  }
}, [submitted]);

「送信ボタンが押されたら送る」のであれば、押されたその場所に書けば済みます

// ○ 押されたときの処理は、押されたところに書く
const handleSubmit = () => {
  sendData();
};

just-calculate.tsx

"use client";

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

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

  const [items, setItems] = useState(["りんご"]);

  // useEffect も state も要らない。ただ計算するだけ
  const count = items.length;

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <p>件数: {count}</p>
        <p className="text-muted-foreground">{items.join(", ")}</p>
      </div>

      <Button size="sm" onClick={() => setItems((current) => [...current, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

4. state を初期化するために使っている

// ✕ id が変わったら入力内容を消したい
useEffect(() => {
  setText("");
}, [userId]);

これは Part 4「useState の応用」でやった key で解決できます。

// ○ key を変えると、React は別物とみなして作り直す
<EditForm key={userId} userId={userId} />

「key が変わると state ごと作り直される」という性質は、 リストと key の章でも触れました。初期化したいなら、作り直させるのがいちばん確実です。

just-calculate.tsx

"use client";

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

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

  const [items, setItems] = useState(["りんご"]);

  // useEffect も state も要らない。ただ計算するだけ
  const count = items.length;

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <p>件数: {count}</p>
        <p className="text-muted-foreground">{items.join(", ")}</p>
      </div>

      <Button size="sm" onClick={() => setItems((current) => [...current, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

5. effect が、次の effect を呼んでいる

いちばん見つけにくい形です。 1 つ 1 つは自然に見えるのに、つなぐと苦しくなります。

useEffect(() => { setSubtotal(...) }, [items]);      // 品目 → 小計
useEffect(() => { setTax(...) }, [subtotal]);        // 小計 → 税
useEffect(() => { setTotal(...) }, [subtotal, tax]); // 税 → 合計
effect を数珠つなぎにした版うまくいかない例

1 回押して、render の数字を見る

品目: 0小計: 0税: 0合計: 0

1 回押しただけで、何度も描き直されます。小計が決まってから税、税が決まってから合計、と順番に待っているためです。 画面には一瞬、税がまだ古い状態も映ります。

その場で計算した版直した例

同じように 1 回押す

品目: 0小計: 0税: 0合計: 0

1 回で終わります。持っているのは品目だけで、 小計も税も合計もそのつど計算しているだけです。

effect-chain.tsx

"use client";

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

/*
  この lint は「effect の中で setState するな」と言う。まったく正しい。
  この章はその禁じ手をあえて踏んで、何が起きるかを見るためのもの。
  下の EffectChain だけが対象で、JustCompute は effect を使っていない。
*/
/* eslint-disable react-hooks/set-state-in-effect */

const prices: Record<string, number> = { りんご: 150, みかん: 100 };

/** effect が次の effect を呼ぶ、数珠つなぎの形 */
export function EffectChain() {
  useTrackDemoRender();

  const [items, setItems] = useState<string[]>([]);
  const [subtotal, setSubtotal] = useState(0);
  const [tax, setTax] = useState(0);
  const [total, setTotal] = useState(0);

  // 1 つめ … 品目が変わったら小計を出す
  useEffect(() => {
    setSubtotal(items.reduce((sum, name) => sum + prices[name], 0));
  }, [items]);

  // 2 つめ … 小計が変わったら税を出す
  useEffect(() => {
    setTax(Math.floor(subtotal * 0.1));
  }, [subtotal]);

  // 3 つめ … 税が変わったら合計を出す
  useEffect(() => {
    setTotal(subtotal + tax);
  }, [subtotal, tax]);

  return (
    <div className="flex flex-col gap-3">
      <div className="flex flex-wrap gap-2">
        <Button
          size="sm"
          onClick={() => setItems((current) => [...current, "りんご"])}
        >
          りんごを足す
        </Button>
        <Button size="sm" variant="outline" onClick={() => setItems([])}>
          空にする
        </Button>
      </div>

      <div className="rounded-lg border p-4">
        <div className="flex flex-col gap-1 font-mono text-sm">
          <span>品目: {items.length} 個</span>
          <span>小計: {subtotal} 円</span>
          <span>税: {tax} 円</span>
          <span>合計: {total} 円</span>
        </div>
      </div>
    </div>
  );
}

/** 連鎖をやめて、その場で計算する */
export function JustCompute() {
  useTrackDemoRender();

  const [items, setItems] = useState<string[]>([]);

  // 持つのは品目だけ。あとは全部その場で出す
  const subtotal = items.reduce((sum, name) => sum + prices[name], 0);
  const tax = Math.floor(subtotal * 0.1);
  const total = subtotal + tax;

  return (
    <div className="flex flex-col gap-3">
      <div className="flex flex-wrap gap-2">
        <Button
          size="sm"
          onClick={() => setItems((current) => [...current, "りんご"])}
        >
          りんごを足す
        </Button>
        <Button size="sm" variant="outline" onClick={() => setItems([])}>
          空にする
        </Button>
      </div>

      <div className="rounded-lg border p-4">
        <div className="flex flex-col gap-1 font-mono text-sm">
          <span>品目: {items.length} 個</span>
          <span>小計: {subtotal} 円</span>
          <span>税: {tax} 円</span>
          <span>合計: {total} 円</span>
        </div>
      </div>
    </div>
  );
}

6. 親に知らせるために使っている

// ✕ 選択が変わったら、親に伝えたい
function Child({ onSelect }) {
  const [selected, setSelected] = useState(null);

  useEffect(() => {
    onSelect(selected);
  }, [selected]);
}

動きはしますが、遠回りです。選ばれた瞬間には、もう分かっているのに、 わざわざ描き直しを 1 回はさんでから伝えています。

// ○ その場で両方やる
function Child({ onSelect }) {
  const [selected, setSelected] = useState(null);

  const handleSelect = (value) => {
    setSelected(value);
    onSelect(value);      // 同じイベントの中で伝える
  };
}

3 番と同じ考え方です。 「値が変わったから」ではなく、「押されたから」で書けないかを先に考えます。

just-calculate.tsx

"use client";

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

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

  const [items, setItems] = useState(["りんご"]);

  // useEffect も state も要らない。ただ計算するだけ
  const count = items.length;

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <p>件数: {count}</p>
        <p className="text-muted-foreground">{items.join(", ")}</p>
      </div>

      <Button size="sm" onClick={() => setItems((current) => [...current, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

では、いつ使うのか

ここまで否定ばかりでしたが、必要な場面はもちろんあります。 共通しているのは、相手が React の外側にいることです。

  • タイマーを動かす / 止める
  • ブラウザのイベントを購読する
  • ブラウザのタブ名や表示位置を操作する
  • React で書かれていないライブラリと接続する

just-calculate.tsx

"use client";

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

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

  const [items, setItems] = useState(["りんご"]);

  // useEffect も state も要らない。ただ計算するだけ
  const count = items.length;

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <p>件数: {count}</p>
        <p className="text-muted-foreground">{items.join(", ")}</p>
      </div>

      <Button size="sm" onClick={() => setItems((current) => [...current, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

理解できたか確かめる

確認クイズ

effect の中で setState しか呼んでいない。これは?

確認クイズ

userId が変わったら、フォームの入力内容を消したい。どうする?

確認クイズ

useEffect の依存配列に、自分がその effect で更新している値が入っている。何を疑う?

just-calculate.tsx

"use client";

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

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

  const [items, setItems] = useState(["りんご"]);

  // useEffect も state も要らない。ただ計算するだけ
  const count = items.length;

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <p>件数: {count}</p>
        <p className="text-muted-foreground">{items.join(", ")}</p>
      </div>

      <Button size="sm" onClick={() => setItems((current) => [...current, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

この章のまとめ

  • 計算できる値は計算する。effect で state に写さない
  • props はそのまま使う。state に写さない
  • ユーザーの操作で起きることはイベントハンドラに書く
  • state を初期化したいときは key で作り直させる
  • 使うのはReact の外側とやり取りするときだけ

just-calculate.tsx

"use client";

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

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

  const [items, setItems] = useState(["りんご"]);

  // useEffect も state も要らない。ただ計算するだけ
  const count = items.length;

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <p>件数: {count}</p>
        <p className="text-muted-foreground">{items.join(", ")}</p>
      </div>

      <Button size="sm" onClick={() => setItems((current) => [...current, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}