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]);動きはします。ですが問題があります。
動くが、遠回りしている
件数: 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]); // 税 → 合計1 回押して、render の数字を見る
1 回押しただけで、何度も描き直されます。小計が決まってから税、税が決まってから合計、と順番に待っているためです。 画面には一瞬、税がまだ古い状態も映ります。
同じように 1 回押す
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>
);
}