Part 6 · 副作用 (useEffect)
クリーンアップ
後片付けを書かないと何が漏れるのか
useEffect で外側に働きかけたなら、やめるときの後片付けも要ります。
タイマーを動かしたら止める。購読したら解除する。 これを書かないと、画面から消えたあとも動き続けるものが裏に残ります。
返した関数が、後片付けになる
useEffect に渡した関数から関数を返すと、 React はそれを後片付けとして扱います。
useEffect(() => {
const timer = setInterval(tick, 1000);
// これが後片付け
return () => clearInterval(timer);
}, []);返した関数が呼ばれるのは、次の 2 つのときです。
- コンポーネントが画面から消えるとき
- 依存配列の値が変わって、effect をやり直す前
隠すとタイマーも止まり、表示すると 0 から始まる
表示してから 0 秒
「隠す」を押すと、Clock が画面から消えます。このとき後片付けが呼ばれ、 タイマーが止まります。もう一度表示すると、 新しく作り直されるので 0 から始まります。
timer.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
function Clock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds((current) => current + 1);
}, 1000);
// 表示をやめるときにタイマーを止める。
// これを書かないと、消えたあともタイマーだけが動き続ける
return () => clearInterval(timer);
}, []);
return (
<p className="rounded-md border p-3 font-mono">
表示してから {seconds} 秒
</p>
);
}
export function Timer() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [isVisible, setIsVisible] = useState(true);
return (
<div className="flex flex-col gap-4">
{isVisible ? (
<Clock />
) : (
<p className="rounded-md border p-3 text-muted-foreground">
非表示(タイマーは止まっている)
</p>
)}
<Button size="sm" onClick={() => setIsVisible(!isVisible)}>
{isVisible ? "隠す" : "表示する"}
</Button>
</div>
);
}書かないと何が起きるか
clearInterval を書かなかったとしましょう。 画面から消えても、タイマーは動き続けます。
- 裏で動き続ける … 見えていない処理が、ずっと CPU を使い続ける
- 増えていく … 表示・非表示を繰り返すたびにタイマーが増え、止まらないまま溜まる
- 消えたものを更新しようとする … すでにない state を更新しようとして、警告やエラーになる
この手の問題はすぐには表面化しません。 しばらく使っているうちに重くなる、という形で出てきます。 原因を探すのが非常に大変な種類のバグです。
timer.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
function Clock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds((current) => current + 1);
}, 1000);
// 表示をやめるときにタイマーを止める。
// これを書かないと、消えたあともタイマーだけが動き続ける
return () => clearInterval(timer);
}, []);
return (
<p className="rounded-md border p-3 font-mono">
表示してから {seconds} 秒
</p>
);
}
export function Timer() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [isVisible, setIsVisible] = useState(true);
return (
<div className="flex flex-col gap-4">
{isVisible ? (
<Clock />
) : (
<p className="rounded-md border p-3 text-muted-foreground">
非表示(タイマーは止まっている)
</p>
)}
<Button size="sm" onClick={() => setIsVisible(!isVisible)}>
{isVisible ? "隠す" : "表示する"}
</Button>
</div>
);
}よくある後片付け
// タイマー
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
// ブラウザのイベント
useEffect(() => {
const onScroll = () => { /* ... */ };
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
// 通信(結果が返る前に消えた場合に備える)
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal });
return () => controller.abort();
}, [url]);removeEventListener には登録したものと同じ関数を渡す必要があります。 だから、その場で書かずに変数に入れておきます。
やり直す前にも呼ばれる
依存配列の値が変わったときは、後片付け → 新しい effect の順で実行されます。
useEffect(() => {
const socket = connect(roomId);
return () => socket.close();
}, [roomId]);roomId が変わると、まず古い接続が閉じられ、 そのあと新しい接続が作られます。古いものが残ったまま新しいものが増える、ということが起きません。
timer.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
function Clock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds((current) => current + 1);
}, 1000);
// 表示をやめるときにタイマーを止める。
// これを書かないと、消えたあともタイマーだけが動き続ける
return () => clearInterval(timer);
}, []);
return (
<p className="rounded-md border p-3 font-mono">
表示してから {seconds} 秒
</p>
);
}
export function Timer() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [isVisible, setIsVisible] = useState(true);
return (
<div className="flex flex-col gap-4">
{isVisible ? (
<Clock />
) : (
<p className="rounded-md border p-3 text-muted-foreground">
非表示(タイマーは止まっている)
</p>
)}
<Button size="sm" onClick={() => setIsVisible(!isVisible)}>
{isVisible ? "隠す" : "表示する"}
</Button>
</div>
);
}理解できたか確かめる
確認クイズ
useEffect から返した関数はいつ呼ばれる?
確認クイズ
clearInterval を書き忘れるとどうなる?
timer.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
function Clock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds((current) => current + 1);
}, 1000);
// 表示をやめるときにタイマーを止める。
// これを書かないと、消えたあともタイマーだけが動き続ける
return () => clearInterval(timer);
}, []);
return (
<p className="rounded-md border p-3 font-mono">
表示してから {seconds} 秒
</p>
);
}
export function Timer() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [isVisible, setIsVisible] = useState(true);
return (
<div className="flex flex-col gap-4">
{isVisible ? (
<Clock />
) : (
<p className="rounded-md border p-3 text-muted-foreground">
非表示(タイマーは止まっている)
</p>
)}
<Button size="sm" onClick={() => setIsVisible(!isVisible)}>
{isVisible ? "隠す" : "表示する"}
</Button>
</div>
);
}この章のまとめ
useEffectから返した関数が後片付けになる- 呼ばれるのは、消えるときとやり直す前
- 書かないと、裏で動き続けるものが溜まっていく。すぐには表面化しないのが厄介
- 始めたら終わらせる。片方を書いた時点でもう片方も書く
- 開発中に 2 回実行されるのは、後片付けの確認のため
timer.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
function Clock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds((current) => current + 1);
}, 1000);
// 表示をやめるときにタイマーを止める。
// これを書かないと、消えたあともタイマーだけが動き続ける
return () => clearInterval(timer);
}, []);
return (
<p className="rounded-md border p-3 font-mono">
表示してから {seconds} 秒
</p>
);
}
export function Timer() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [isVisible, setIsVisible] = useState(true);
return (
<div className="flex flex-col gap-4">
{isVisible ? (
<Clock />
) : (
<p className="rounded-md border p-3 text-muted-foreground">
非表示(タイマーは止まっている)
</p>
)}
<Button size="sm" onClick={() => setIsVisible(!isVisible)}>
{isVisible ? "隠す" : "表示する"}
</Button>
</div>
);
}