Part 4 · 状態 (state)
27 / 64 章
state は「その瞬間の写真」
setState した直後に値が変わっていない理由
setCount を呼んだ直後に count を見ると、まだ古い値のままです。
これは不具合ではなく、React の設計です。 ただ、知らないと必ず引っかかります。 「3 回呼んだのに 1 しか増えない」という形で現れます。
3 回呼んでも 1 しか増えない
setCount(count + 1) を 3 回並べれば 3 増えそうです。 押してみてください。
1 ずつしか増えない
1 しか増えません。理由は、この関数の実行中、count がずっと同じ値だからです。
// count が 0 のときに押すと…
setCount(count + 1); // setCount(0 + 1) → 1 にして、と依頼
setCount(count + 1); // setCount(0 + 1) → やっぱり 1 にして
setCount(count + 1); // setCount(0 + 1) → やっぱり 1 にして3 回とも「1 にしてください」と頼んでいます。 結果が 1 になるのは当然です。
snapshot.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Snapshot() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [count, setCount] = useState(0);
const addThree = () => {
// 3 回呼んでいるが、この関数の中では count はずっと同じ値のまま。
// 3 回とも「0 + 1」を依頼していることになる
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold tabular-nums">
{count}
</span>
<Button size="sm" onClick={addThree}>
3 増やすつもり
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}まとめて処理される(バッチング)
いまのは同じ state を 3 回でした。 では、別々の state を 3 つ更新したらどうなるでしょうか。 3 回描き直されそうに思えます。
const updateAll = () => {
setName(...);
setAge(...);
setAdmin(...);
};押すたびに render はいくつ増える?
1 しか増えません。3 つとも別の state なのに、描き直しは 1 回にまとめられました。 これをバッチングと呼びます。
もしまとめられなかったら、途中の状態が一瞬だけ画面に出てしまいます。 名前だけ変わって年齢は古いまま、という中途半端な画面です。 まとめることで、そういう状態を見せずに済んでいます。
待つ処理をはさんでも、まとめられる
もう 1 つのボタンは、await で 0.3 秒待ってから同じ 3 つを更新します。 こちらも render は 1 しか増えません。
batching.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Batching() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [name, setName] = useState("さとう");
const [age, setAge] = useState(20);
const [admin, setAdmin] = useState(false);
// 3 つとも別々の state。それでも描き直しは 1 回にまとめられる
const updateAll = () => {
setName((current) => (current === "さとう" ? "すずき" : "さとう"));
setAge((current) => current + 1);
setAdmin((current) => !current);
};
// 待つ処理をはさんでも、まとめられる
const updateAfterWait = async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
updateAll();
};
return (
<div className="flex flex-col gap-3">
<div className="rounded-md border p-3 font-mono text-sm">
{`{ name: "${name}", age: ${age}, admin: ${String(admin)} }`}
</div>
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={updateAll}>
3 つまとめて更新する
</Button>
<Button size="sm" variant="outline" onClick={updateAfterWait}>
0.3 秒待ってから更新する
</Button>
</div>
</div>
);
}state は「その回の写真」
前の章でやったとおり、コンポーネントは state が変わるたびに実行し直されます。
そして1 回の実行の間、state の値は変わりません。 その実行が始まった時点の値のまま、最後まで固定されています。 撮った写真が途中で変化しないのと同じです。
setCount は「次はこの値で実行してください」という予約です。いま動いている実行には影響しません。
snapshot.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Snapshot() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [count, setCount] = useState(0);
const addThree = () => {
// 3 回呼んでいるが、この関数の中では count はずっと同じ値のまま。
// 3 回とも「0 + 1」を依頼していることになる
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold tabular-nums">
{count}
</span>
<Button size="sm" onClick={addThree}>
3 増やすつもり
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}いまの値を使いたいときは関数を渡す
「1 増やす」のようにいまの値をもとに決めたいときは、 値ではなく関数を渡します。
setCount((current) => current + 1);こう書くと、React は更新するときにそのときの最新の値を current に入れて呼びます。 自分で値を計算しないので、古い値を使ってしまう心配がありません。
今度はちゃんと 3 増える
React は依頼を順番に適用します。 0 に対して +1 して 1、その 1 に +1 して 2、その 2 に +1 して 3。積み上がるので、3 増えます。
どちらを使えばいいか
- いまの値を使うなら関数を渡す(+1、切り替え、追加など)
- 関係のない値を入れるなら、そのまま渡してよい(リセット、入力値の反映など)
迷ったら関数を渡す形にしておけば、まず間違いません。
updater.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Updater() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [count, setCount] = useState(0);
const addThree = () => {
// 値ではなく「いまの値をどう変えるか」を渡す。
// React が順番に適用するので、3 回ぶんが積み上がる
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
};
return (
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold tabular-nums">
{count}
</span>
<Button size="sm" onClick={addThree}>
3 増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}直後に読んでも変わっていない
const handleClick = () => {
setCount(count + 1);
console.log(count); // まだ古い値が出る
};「更新に時間がかかっているのでは」と思うかもしれませんが、違います。この実行における count は、最初から最後まで同じ値だからです。 待っても変わりません。
新しい値を使いたい処理があるなら、 その値を変数に入れておくか、次の実行で扱います。
const next = count + 1;
setCount(next);
console.log(next); // これなら新しい値snapshot.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Snapshot() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [count, setCount] = useState(0);
const addThree = () => {
// 3 回呼んでいるが、この関数の中では count はずっと同じ値のまま。
// 3 回とも「0 + 1」を依頼していることになる
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold tabular-nums">
{count}
</span>
<Button size="sm" onClick={addThree}>
3 増やすつもり
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}理解できたか確かめる
確認クイズ
setCount(count + 1) を 3 回並べると、count はいくつ増える?
確認クイズ
setCount((c) => c + 1) と書く利点は?
updater.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Updater() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [count, setCount] = useState(0);
const addThree = () => {
// 値ではなく「いまの値をどう変えるか」を渡す。
// React が順番に適用するので、3 回ぶんが積み上がる
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
};
return (
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold tabular-nums">
{count}
</span>
<Button size="sm" onClick={addThree}>
3 増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}この章のまとめ
- 1 回の実行の間、state の値は固定されている
- だから
setCountの直後に読んでも古い値のまま - 値を渡す形で複数回呼んでも積み上がらない
- いまの値をもとに決めたいときは
setCount((c) => c + 1)と関数を渡す
updater.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Updater() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [count, setCount] = useState(0);
const addThree = () => {
// 値ではなく「いまの値をどう変えるか」を渡す。
// React が順番に適用するので、3 回ぶんが積み上がる
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
};
return (
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold tabular-nums">
{count}
</span>
<Button size="sm" onClick={addThree}>
3 増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}