その useEffect は要らない

Part 6 · 副作用 (useEffect)

その useEffect は要らない

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

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

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

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

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-3 でやった二重管理そのもの

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

ただ計算する直した例

useEffect も state も要らない

件数: 1

りんご

effect-derived.tsx

"use client";

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

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

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([...items, "みかん"])}>
        追加する
      </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";

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

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

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([...items, "みかん"])}>
        追加する
      </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([...items, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

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

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

これは Part 3 でやった 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([...items, "みかん"])}>
        追加する
      </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([...items, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}

理解できたか確かめる

確認クイズ

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

確認クイズ

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

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([...items, "みかん"])}>
        追加する
      </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([...items, "みかん"])}>
        追加する
      </Button>
    </div>
  );
}