Part 4 · 状態 (state)
useState
なぜ普通の変数ではダメなのか
ここからが React の中心です。
Part 1 で「画面は状態から決まる」と書きました。 その状態を持つための道具が useState です。
まずは「なぜ、ふつうの変数ではダメなのか」から始めます。 ここを飛ばすと、useState がただのおまじないになってしまいます。
ふつうの変数では動かない
数を持って、ボタンで増やす。let で変数を作れば済みそうに見えます。
押しても 0 のまま
動きません。Part 2 の props のときと同じで、変数の中身は実際に増えています(コンソールに出ています)。 画面に反映されないだけです。
理由は 2 つある
ひとつめ。React はその変数のことを知りません。 知らない値が変わっても、画面を描き直すきっかけになりません。
ふたつめ。こちらのほうが重要です。仮に描き直されても、値は 0 に戻ります。
描き直すというのは、この関数をもう一度実行することです。 関数が実行されれば let count = 0 がまた走ります。 増やした結果はどこにも残りません。
plain-variable.tsx
"use client";
/*
この書き方は React のルール違反なので、本来は lint に止められる。
「なぜ動かないのか」を実際に動かして見せるために、この 1 ファイルだけ黙らせている。
*/
/* eslint-disable react-hooks/immutability */
import { Button } from "@/components/ui/button";
export function PlainVariable() {
// ふつうの変数で数を持ってみる
let count = 0;
const increase = () => {
count = count + 1;
console.log("count は", count, "になった。でも画面は変わらない");
};
return (
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold tabular-nums">
{count}
</span>
<Button size="sm" onClick={increase}>
増やす
</Button>
</div>
);
}useState に預ける
const [count, setCount] = useState(0);この 1 行がやっていることは、こうです。
useState(0)… React に「0 から始まる値を覚えておいて」と頼むcount… いまの値setCount… 変更を React に伝えるための関数
角括弧は、Part 0-4 でやった配列の分割代入です。useState が 2 つ入った配列を返すので、 それを順番に受け取っているだけです。React 専用の記法ではありません。
今度はちゃんと増える
値は React が保管しているので、関数が何度実行されても消えません。 そして setCount を呼ぶと、React は「値が変わった」と知ることができます。
with-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function WithState() {
// React に「この値を覚えておいて」と預ける
const [count, setCount] = useState(0);
const increase = () => {
// 直接書き換えず、React に「こう変えて」と伝える
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={increase}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}setCount は画面を書き換えていない
ここが誤解されやすいところです。setCount は、画面のどこかを直接書き換えているわけではありません。
setCount(1) は React への依頼です。 依頼を受けた React は、この関数をもう一度実行します。 そのとき useState が返す値が 1 になっているので、 新しい JSX には 1 が入り、結果として画面が変わります。
with-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function WithState() {
// React に「この値を覚えておいて」と預ける
const [count, setCount] = useState(0);
const increase = () => {
// 直接書き換えず、React に「こう変えて」と伝える
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={increase}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}useState の決まりごと
useState のような use で始まる関数をフックと呼びます。 フックには、守らなければいけない決まりが 2 つあります。
// ○ コンポーネントの一番上に書く
function Counter() {
const [count, setCount] = useState(0);
...
}
// ✕ 条件の中に書いてはいけない
function Counter() {
if (isReady) {
const [count, setCount] = useState(0);
}
...
}- コンポーネントの一番上で呼ぶ。 if や for、関数の中に入れない
- 呼べるのはコンポーネントの中だけ。 ふつうの関数の中では使えない
React は呼ばれた順番で state を管理しているため、 実行のたびに順番が変わると対応づけが崩れてしまうからです。 この決まりも lint が見張ってくれます。
with-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function WithState() {
// React に「この値を覚えておいて」と預ける
const [count, setCount] = useState(0);
const increase = () => {
// 直接書き換えず、React に「こう変えて」と伝える
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={increase}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}理解できたか確かめる
確認クイズ
ふつうの変数を書き換えても画面が変わらないのはなぜ?
確認クイズ
setCount(1) を呼ぶと何が起きる?
with-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function WithState() {
// React に「この値を覚えておいて」と預ける
const [count, setCount] = useState(0);
const increase = () => {
// 直接書き換えず、React に「こう変えて」と伝える
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={increase}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}この章のまとめ
- ふつうの変数では、再実行のたびに初期値へ戻るので画面は変わらない
useStateは「値を覚えておく」「変更を知らせる」を引き受ける[count, setCount]は配列の分割代入。特別な記法ではないsetCountは画面を書き換えるのではなく、コンポーネントを実行し直させる- フックはコンポーネントの一番上で、条件の外で呼ぶ
with-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function WithState() {
// React に「この値を覚えておいて」と預ける
const [count, setCount] = useState(0);
const increase = () => {
// 直接書き換えず、React に「こう変えて」と伝える
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={increase}>
増やす
</Button>
<Button size="sm" variant="outline" onClick={() => setCount(0)}>
リセット
</Button>
</div>
);
}