Part 4 · 状態 (state)
state は最小限にする
計算で求まる値を state にしてはいけない
useState が使えるようになると、 画面に出したいものを片っ端から state にしたくなります。
ですが state は増やすほど壊れやすくなります。 持つべきものと、持ってはいけないものがあります。
判断の基準はひとつだけです。他の state から計算できるなら、state にしない。
同じことを 2 か所で持つと、ずれる
リストと、その件数を表示する画面を考えます。 素直に作ると、両方を state にしてしまいがちです。
const [items, setItems] = useState(["りんご"]);
const [count, setCount] = useState(1);こうすると、items を変えるたびに count も直す必要が出てきます。下のデモで「空にする」を押してみてください。
中身は空なのに、件数だけ残る
件数: 1
中身: りんご
中身は空なのに、件数は残ったままです。clear の中で setCount(0) を書き忘れたためです。
これは「うっかり」で片付く話ではありません。2 か所を手で合わせ続けなければならない作りにした時点で、 いつか必ず起きます。更新する場所が増えるほど、確率は上がります。
duplicated-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function DuplicatedState() {
const [items, setItems] = useState(["りんご"]);
// 件数も state として別に持ってしまっている
const [count, setCount] = useState(1);
const add = () => {
setItems([...items, "りんご"]);
setCount(count + 1); // 2 か所を手で合わせている
};
const clear = () => {
setItems([]);
// ここで setCount(0) を書き忘れた。件数だけが残る
};
return (
<div className="flex flex-col gap-4">
<div className="rounded-md border p-3">
<p>件数: {count}</p>
<p className="text-muted-foreground">
中身: {items.length === 0 ? "(空)" : items.join(", ")}
</p>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={add}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={clear}>
空にする
</Button>
</div>
</div>
);
}計算できるものは、計算する
件数は items から必ず求まります。 なら、持つ必要はありません。
const [items, setItems] = useState(["りんご"]);
// state ではなく、ただの変数
const count = items.length;コンポーネントは state が変わるたびに実行し直されるので、この行も毎回計算し直されます。 つまり常に最新です。
更新の書き忘れが起こりえない
件数: 1
中身: りんご
clear のような処理も、setItems([]) だけで済みます。合わせる相手がいないので、ずれようがありません。
derived-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function DerivedState() {
// state はこれだけ
const [items, setItems] = useState(["りんご"]);
// 件数は 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.length === 0 ? "(空)" : items.join(", ")}
</p>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={() => setItems([...items, "りんご"])}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setItems([])}>
空にする
</Button>
</div>
</div>
);
}よくある「state にしなくていいもの」
const [items, setItems] = useState<Item[]>([]);
const [keyword, setKeyword] = useState("");
// どれも計算で出せる。state にしない
const count = items.length;
const isEmpty = items.length === 0;
const total = items.reduce((sum, item) => sum + item.price, 0);
const found = items.filter((item) => item.name.includes(keyword));
const canSubmit = keyword.trim() !== "";ここで state なのは items と keyword の 2 つだけです。残りは全部そこから決まります。
「この画面の状態は何か」を考えるとき、これ以上減らせない情報は何かという問い方をすると、 自然と最小限になります。
derived-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function DerivedState() {
// state はこれだけ
const [items, setItems] = useState(["りんご"]);
// 件数は 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.length === 0 ? "(空)" : items.join(", ")}
</p>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={() => setItems([...items, "りんご"])}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setItems([])}>
空にする
</Button>
</div>
</div>
);
}理解できたか確かめる
確認クイズ
items と、その件数 count を両方 state にすると何が起きる?
確認クイズ
次のうち、state にすべきなのはどれ?
derived-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function DerivedState() {
// state はこれだけ
const [items, setItems] = useState(["りんご"]);
// 件数は 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.length === 0 ? "(空)" : items.join(", ")}
</p>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={() => setItems([...items, "りんご"])}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setItems([])}>
空にする
</Button>
</div>
</div>
);
}この章のまとめ
- 他の state から計算できるものは、state にしない
- 二重に持つと、更新のたびに手で合わせる必要が生まれ、いつかずれる
- 計算した値はコンポーネントの実行のたびに求め直されるので、常に最新
- 件数・合計・絞り込み結果・押せるかどうかは、たいてい計算で出せる
- state を決めるときは「これ以上減らせない情報は何か」と考える
derived-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function DerivedState() {
// state はこれだけ
const [items, setItems] = useState(["りんご"]);
// 件数は 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.length === 0 ? "(空)" : items.join(", ")}
</p>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={() => setItems([...items, "りんご"])}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setItems([])}>
空にする
</Button>
</div>
</div>
);
}