Part 4 · 状態 (state)
25 / 64 章
useState の使い方いろいろ
実際どう使われているか。よく出てくる 4 つの形
前の章で useState の形は分かりました。 ですが「数を数える」以外に何ができるのか、 まだ想像しにくいと思います。
実際のアプリで useState がどう使われているかを、よく出てくる順に 4 つ並べます。
どれも今日から使うものです。 知っておくと効く応用は、次の章にまとめてあります。
1. 切り替える(いちばん多い)
開いている / 閉じている、表示 / 非表示、オン / オフ。2 択のものは useState の出番の 8 割です。
const [isOpen, setIsOpen] = useState(false);切り替えるときは、いまの反対を入れます。
setIsOpen(!isOpen);押すたびに中身が出たり消えたりする
Part 3 でやった && と組み合わせるのが定番の形です。「true のときだけ出す」と読めます。
{isOpen && <p>中身</p>}toggle.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Toggle() {
useTrackDemoRender();
// いちばんよく使う形。true / false の 2 択
const [isOpen, setIsOpen] = useState(false);
return (
<div className="flex flex-col gap-3">
{/* いまの反対を入れる = 切り替え */}
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "閉じる" : "開く"}
</Button>
{isOpen && (
<p className="rounded-md border p-3 text-sm text-muted-foreground">
開いたときだけ出てくる中身です。
</p>
)}
</div>
);
}2. どれが選ばれているかを覚える
タブ、プラン選択、一覧から 1 件選ぶ。「いま選ばれているもの」を 1 つだけ持ちます。
const [selected, setSelected] = useState<string | null>(null);ここで大事なのが null です。「まだ選んでいない」も、立派な状態のひとつだからです。 空文字 "" で代用すると、 「選んでいない」のか「空という名前を選んだ」のか区別できなくなります。
押すと色が変わり、下の文が変わる
まだ選んでいません
selection.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
const plans = ["お試し", "標準", "たっぷり"];
export function Selection() {
useTrackDemoRender();
// 「まだ選んでいない」を null で表す。型は string か null
const [selected, setSelected] = useState<string | null>(null);
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap gap-2">
{plans.map((plan) => (
<Button
key={plan}
size="sm"
// 選ばれているかどうかは、比べて求める。持たない
variant={selected === plan ? "default" : "outline"}
onClick={() => setSelected(plan)}
>
{plan}
</Button>
))}
</div>
<p className="text-sm text-muted-foreground">
{selected === null ? "まだ選んでいません" : `${selected} を選びました`}
</p>
</div>
);
}3. 「途中の状態」を持つ
送信中、読み込み中、失敗した。時間のかかる処理には、必ず途中があります。
素直に書くと、真偽値を並べたくなります。
const [isLoading, setIsLoading] = useState(false);
const [isDone, setIsDone] = useState(false);
const [isError, setIsError] = useState(false);動きます。ですが穴があります。 デモの「壊す」ボタンを押してみてください。
「壊す」を押すと、ありえない表示になる
待機中
「送信中」と「送信しました」と「失敗しました」が同時に出ました。現実にはありえない状態です。
真偽値が 3 つあると、組み合わせは 2 × 2 × 2 = 8 通り。 そのうち意味があるのは 4 通りだけです (まだ何もしていない / 送信中 / 完了 / 失敗)。 残りの 4 通りは、書けてしまうけれど存在してはいけない状態です。
boolean-soup.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function BooleanSoup() {
useTrackDemoRender();
// 真偽値を並べると、ありえない組み合わせが作れてしまう
const [isLoading, setIsLoading] = useState(false);
const [isDone, setIsDone] = useState(false);
const [isError, setIsError] = useState(false);
const send = (shouldFail: boolean) => {
setIsLoading(true);
setIsDone(false);
setIsError(false);
setTimeout(() => {
setIsLoading(false);
if (shouldFail) setIsError(true);
else setIsDone(true);
}, 800);
};
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2">
<Button size="sm" onClick={() => send(false)}>
送信(成功する)
</Button>
<Button size="sm" variant="outline" onClick={() => send(true)}>
送信(失敗する)
</Button>
<Button
size="sm"
variant="outline"
// 3 つとも true。画面の指示どおりには、こうも書けてしまう
onClick={() => {
setIsLoading(true);
setIsDone(true);
setIsError(true);
}}
>
壊す
</Button>
</div>
<p className="text-sm">
{isLoading && "送信中… "}
{isDone && "送信しました "}
{isError && "失敗しました "}
{!isLoading && !isDone && !isError && "待機中"}
</p>
</div>
);
}4. とりうる状態を、並べて書く
解決は簡単です。state を 1 つにして、とりうる値を並べます。
type Status = "idle" | "sending" | "done" | "error";
const [status, setStatus] = useState<Status>("idle");縦棒(|)は「または」です。Part 0 の TypeScript で出てきました。この 4 つ以外にはなれません。setStatus("おわり") と書けば、その場で赤線が出ます。
「壊す」ボタンが作れない
待機中
今度は「壊す」ボタンを置けませんでした。status は必ず 1 つの値しか持てないので、 3 つ同時に true という状態が作れないからです。
setStatus("sending"); // これだけで、他は自動的に否定されるリセットの書き忘れも起きません。次の状態を入れれば、前の状態は必ず消えます。
status-union.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// とりうる状態を並べて書く。この 4 つ以外にはなれない
type Status = "idle" | "sending" | "done" | "error";
export function StatusUnion() {
useTrackDemoRender();
const [status, setStatus] = useState<Status>("idle");
const send = (shouldFail: boolean) => {
setStatus("sending");
setTimeout(() => {
setStatus(shouldFail ? "error" : "done");
}, 800);
};
// 表示は 1 か所にまとまる。どの状態でも必ず 1 つだけ選ばれる
const message = {
idle: "待機中",
sending: "送信中…",
done: "送信しました",
error: "失敗しました",
}[status];
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2">
<Button
size="sm"
onClick={() => send(false)}
disabled={status === "sending"}
>
送信(成功する)
</Button>
<Button
size="sm"
variant="outline"
onClick={() => send(true)}
disabled={status === "sending"}
>
送信(失敗する)
</Button>
</div>
<p className="text-sm">{message}</p>
</div>
);
}理解できたか確かめる
確認クイズ
「まだ選んでいない」を表すのに適しているのは?
確認クイズ
真偽値を 3 つ並べる代わりに union を使うと、何が良くなる?
確認クイズ
開いているかどうかの state に付ける名前として、いちばんよいのは?
status-union.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// とりうる状態を並べて書く。この 4 つ以外にはなれない
type Status = "idle" | "sending" | "done" | "error";
export function StatusUnion() {
useTrackDemoRender();
const [status, setStatus] = useState<Status>("idle");
const send = (shouldFail: boolean) => {
setStatus("sending");
setTimeout(() => {
setStatus(shouldFail ? "error" : "done");
}, 800);
};
// 表示は 1 か所にまとまる。どの状態でも必ず 1 つだけ選ばれる
const message = {
idle: "待機中",
sending: "送信中…",
done: "送信しました",
error: "失敗しました",
}[status];
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2">
<Button
size="sm"
onClick={() => send(false)}
disabled={status === "sending"}
>
送信(成功する)
</Button>
<Button
size="sm"
variant="outline"
onClick={() => send(true)}
disabled={status === "sending"}
>
送信(失敗する)
</Button>
</div>
<p className="text-sm">{message}</p>
</div>
);
}この章のまとめ
- 切り替え(
!isOpen)が いちばんよく使う形。名前はis/hasで始める - 選択中のものは 1 つだけ持ち、 「選ばれているか」は比べて求める。 「未選択」は
null - 真偽値が並びはじめたらunion にまとめる。 ありえない状態を書けなくする
- 応用(初期値の遅延評価、
keyでのリセット)は次の章
toggle.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function Toggle() {
useTrackDemoRender();
// いちばんよく使う形。true / false の 2 択
const [isOpen, setIsOpen] = useState(false);
return (
<div className="flex flex-col gap-3">
{/* いまの反対を入れる = 切り替え */}
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "閉じる" : "開く"}
</Button>
{isOpen && (
<p className="rounded-md border p-3 text-sm text-muted-foreground">
開いたときだけ出てくる中身です。
</p>
)}
</div>
);
}