本文へスキップ
無限ループにしない

Part 6 · 副作用 (useEffect)

36 / 64

無限ループにしない

依存配列に入れた値を、その中で更新してはいけない

useEffect を書いていて、ブラウザが固まった経験はありませんか。 初めて書く人がほぼ必ず一度は踏みます。

原因は決まっています。依存配列に入れた値を、その effect の中で更新している。 これだけです。

仕組みが分かれば、書く前に気づけるようになります。

止まらなくなる書き方

「動かしてみる」を押すと、count が一気に増えます。 押しっぱなしにしているわけでも、繰り返し押しているわけでもありません。

依存配列の値を、その中で更新するうまくいかない例

一度押しただけで、50 まで駆け上がる

count: 0

render の数字を見てください。一度の操作で何十回も描き直されています

loop.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 */

// 教材用の安全装置。これがなければ、ブラウザが固まるまで止まらない
const LIMIT = 50;

export function Loop() {
  useTrackDemoRender();

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

  useEffect(() => {
    if (!running) return;
    if (count >= LIMIT) return; // ← この 1 行がなければ止まらない

    // 依存配列に入っている count を、その中で更新している
    setCount(count + 1);
  }, [count, running]);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">count: {count}</p>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setRunning(!running)}>
          {running ? "止める" : "動かしてみる"}
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            setRunning(false);
            setCount(0);
          }}
        >
          最初から
        </Button>
      </div>
    </div>
  );
}

なぜ止まらないのか

useEffect は、依存配列の値が変わったときに実行されるのでした。 そして、その中で setCount を呼んでいます。

effect が動く依存が変わったので実行state を変えるsetCount を呼ぶ描き直されるコンポーネントが再実行依存が変わったcount が新しい値に
どこにも出口がない。止めるには、この輪のどこかを切る必要がある

count を更新すると、コンポーネントが描き直されます。 描き直されると count は新しい値になっています。 依存配列の値が変わったので、React はまた effect を実行します。 そしてまた count を更新する。

自分の実行が、自分の次の実行を呼んでいます。どこにも終わりがありません。

loop.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 */

// 教材用の安全装置。これがなければ、ブラウザが固まるまで止まらない
const LIMIT = 50;

export function Loop() {
  useTrackDemoRender();

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

  useEffect(() => {
    if (!running) return;
    if (count >= LIMIT) return; // ← この 1 行がなければ止まらない

    // 依存配列に入っている count を、その中で更新している
    setCount(count + 1);
  }, [count, running]);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">count: {count}</p>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setRunning(!running)}>
          {running ? "止める" : "動かしてみる"}
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            setRunning(false);
            setCount(0);
          }}
        >
          最初から
        </Button>
      </div>
    </div>
  );
}

入れた覚えがないのにループする

こちらは count を依存配列に入れていません。 それでも止まりません。

オブジェクトを依存配列に入れるうまくいかない例

count は依存配列に入っていないのに、やはり駆け上がる

count: 0

犯人は options です。

const options = { unit: "回" };

この行は描き直されるたびに実行されます。 つまり毎回新しいオブジェクトが作られています。 中身は同じでも、React から見れば別のものです。

Part 4 の「オブジェクトと配列の更新」 でやった「React は同じものかどうかだけを見る」が、ここでも効いてきます。 毎回別のものが渡ってくるので、React は毎回「依存が変わった」と判断します。

object-loop.tsx

"use client";

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

/*
  この章の題材そのもの。lint は「options を useMemo で包め」と言ってくるが、
  ここでは「包まないと何が起きるか」を見せたいので、わざと従っていない。
  effect の中で setState しているのも同じ理由。
*/
/* eslint-disable react-hooks/set-state-in-effect */
/* eslint-disable react-hooks/exhaustive-deps */

const LIMIT = 50;

export function ObjectLoop() {
  useTrackDemoRender();

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

  // 描き直されるたびに新しく作られるオブジェクト。
  // 中身は毎回同じでも、React から見ると「別のもの」
  const options = { unit: "回" };

  useEffect(() => {
    if (!running) return;

    // count は依存配列に入っていない。それでも止まらない
    setCount((current) => (current >= LIMIT ? current : current + 1));
  }, [running, options]);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">
        count: {count} {options.unit}
      </p>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setRunning(!running)}>
          {running ? "止める" : "動かしてみる"}
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            setRunning(false);
            setCount(0);
          }}
        >
          最初から
        </Button>
      </div>
    </div>
  );
}

直し方

直す方向は 3 つあります。上から順に検討してください。

1. そもそも effect が要らないのでは、と疑う

前の章でやったとおりです。計算できる値なら、effect も state も要りません。ループの多くは、この見直しだけで消えます。

2. 依存を減らす

いまの値をもとに更新するなら、関数を渡す形にします。そうすれば、 その値を依存配列に入れる必要がなくなります。

// ✕ count を使うので、依存配列に入れざるを得ない
setCount(count + 1);

// ○ 最新の値は React が渡してくれる。count は依存に不要
setCount((current) => current + 1);
依存を減らして書き直す直した例

一定の間隔で増え、止めれば止まる

count: 0

依存配列は [running] だけになりました。タイマーを仕掛けるのは 1 回きりで、 あとはタイマー自身が数を増やしています。 effect が自分を呼び直すことはありません。

3. 値そのものを安定させる

どうしてもオブジェクトや関数を依存配列に入れたいなら、毎回作り直されないようにします

// コンポーネントの外に出す(いちばん単純)
const options = { unit: "回" };

function Counter() { ... }

外に出せない場合は、useMemo useCallback という道具で、同じものを使い回すようにします。 Part 8 で扱います。

fixed-loop.tsx

"use client";

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

export function FixedLoop() {
  useTrackDemoRender();

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

  useEffect(() => {
    if (!running) return;

    // 一度だけタイマーを仕掛けて、あとは任せる。
    // 関数を渡す形なので、count を依存配列に入れる必要がない
    const timer = setInterval(() => {
      setCount((current) => current + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, [running]); // running が変わったときだけ動く

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">count: {count}</p>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setRunning(!running)}>
          {running ? "止める" : "動かしてみる"}
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            setRunning(false);
            setCount(0);
          }}
        >
          最初から
        </Button>
      </div>
    </div>
  );
}

理解できたか確かめる

確認クイズ

useEffect が無限ループになる、いちばん多い原因は?

確認クイズ

count を依存配列に入れていないのにループする。考えられる原因は?

確認クイズ

setCount(count + 1) を setCount((c) => c + 1) に変える利点は?

loop.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 */

// 教材用の安全装置。これがなければ、ブラウザが固まるまで止まらない
const LIMIT = 50;

export function Loop() {
  useTrackDemoRender();

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

  useEffect(() => {
    if (!running) return;
    if (count >= LIMIT) return; // ← この 1 行がなければ止まらない

    // 依存配列に入っている count を、その中で更新している
    setCount(count + 1);
  }, [count, running]);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">count: {count}</p>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setRunning(!running)}>
          {running ? "止める" : "動かしてみる"}
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            setRunning(false);
            setCount(0);
          }}
        >
          最初から
        </Button>
      </div>
    </div>
  );
}

この章のまとめ

  • 依存配列に入れた値を、その中で更新すると止まらなくなる
  • オブジェクト・配列・関数は描き直しのたびに新しく作られる。 依存配列に入れると、中身が同じでも毎回実行される
  • 直す順番は、①effect が要るか疑う ②依存を減らす ③値を安定させる
  • いまの値を使うなら setCount((c) => c + 1)。 これだけで依存が 1 つ減る

fixed-loop.tsx

"use client";

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

export function FixedLoop() {
  useTrackDemoRender();

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

  useEffect(() => {
    if (!running) return;

    // 一度だけタイマーを仕掛けて、あとは任せる。
    // 関数を渡す形なので、count を依存配列に入れる必要がない
    const timer = setInterval(() => {
      setCount((current) => current + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, [running]); // running が変わったときだけ動く

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">count: {count}</p>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setRunning(!running)}>
          {running ? "止める" : "動かしてみる"}
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            setRunning(false);
            setCount(0);
          }}
        >
          最初から
        </Button>
      </div>
    </div>
  );
}