本文へスキップ
useState の使い方いろいろ

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 つ並べるうまくいかない例

「壊す」を押すと、ありえない表示になる

待機中

「送信中」と「送信しました」と「失敗しました」が同時に出ました。現実にはありえない状態です。

真偽値が 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("おわり") と書けば、その場で赤線が出ます。

状態を 1 つにまとめる直した例

「壊す」ボタンが作れない

待機中

今度は「壊す」ボタンを置けませんでした。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>
  );
}