useState

Part 4 · 状態 (state)

useState

なぜ普通の変数ではダメなのか

ここからが React の中心です。

Part 1 で「画面は状態から決まる」と書きました。 その状態を持つための道具useState です。

まずは「なぜ、ふつうの変数ではダメなのか」から始めます。 ここを飛ばすと、useState がただのおまじないになってしまいます。

ふつうの変数では動かない

数を持って、ボタンで増やす。let で変数を作れば済みそうに見えます。

ふつうの変数で作ったカウンターうまくいかない例

押しても 0 のまま

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 専用の記法ではありません。

useState で作ったカウンター直した例

今度はちゃんと増える

0

値は 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)こう変えてと伝えるもう一度実行関数が呼び直される新しい画面count は 1 になっている
画面を書き換えているのではなく、関数がもう一度実行されている

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>
  );
}