本文へスキップ
useState

Part 4 · 状態 (state)

23 / 64

useState

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

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

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

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

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

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

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

押しても 0 のまま

0

動きません。Part 2 の props のときと同じで、変数の中身は実際に増えています(コンソールに出ています)。 画面に反映されないだけです。

理由は 2 つある

ひとつめ。React はその変数のことを知りません。 知らない値が変わっても、画面を描き直すきっかけになりません。

ふたつめ。こちらのほうが重要です。仮に描き直されても、値は 0 に戻ります。

描き直すというのは、この関数をもう一度実行することです。 関数が実行されれば let count = 0 がまた走ります。 増やした結果はどこにも残りません。

plain-variable.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";

/*
  この書き方は React のルール違反なので、本来は lint に止められる。
  「なぜ動かないのか」を実際に動かして見せるために、この 1 ファイルだけ黙らせている。
*/
/* eslint-disable react-hooks/immutability */

export function PlainVariable() {
  // このカードが描き直された回数を数えるための 1 行。
  // 教材の仕掛けであって、React の機能ではない
  useTrackDemoRender();

  // ふつうの変数で数を持ってみる
  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 の「分割代入とスプレッド構文」 でやった配列の分割代入です。useState が 2 つ入った配列を返すので、 それを順番に受け取っているだけです。React 専用の記法ではありません。

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

今度はちゃんと増える

0

値は React が保管しているので、関数が何度実行されても消えません。 そして setCount を呼ぶと、React は「値が変わった」と知ることができます。

今度は押すたびにカードが光り、render の数字が増えます。 さきほどのデモとの違いは、そこにも出ています。

with-state.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function WithState() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  // 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 { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function WithState() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  // 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 のような use で始まる関数をフックと呼びます。

フックには守らないといけない決まりが 2 つあります。 破ると直しにくい壊れ方をするので、次の章でまとめて扱います

with-state.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function WithState() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  // 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 { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function WithState() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  // 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 { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function WithState() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

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