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 を呼んでいます。
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>
);
}