Part 6 · 副作用 (useEffect)
その useEffect は要らない
書きがちなアンチパターンを 4 つ潰す
前の章で「useEffect は React の外側と同期するための道具」と書きました。
裏を返すと、外側が出てこないなら要らないということです。
ここでは、実際によく書かれてしまう形を 4 つ取り上げて、 それぞれどう書けばよかったのかを見ます。
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-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>
);
}