state は「その瞬間の写真」

Part 4 · 状態 (state)

state は「その瞬間の写真」

setState した直後に値が変わっていない理由

setCount を呼んだ直後に count を見ると、まだ古い値のままです。

これは不具合ではなく、React の設計です。 ただ、知らないと必ず引っかかります。 「3 回呼んだのに 1 しか増えない」という形で現れます。

3 回呼んでも 1 しか増えない

setCount(count + 1) を 3 回並べれば 3 増えそうです。 押してみてください。

3 回呼んだつもりうまくいかない例

1 ずつしか増えない

0

1 しか増えません。理由は、この関数の実行中、count がずっと同じ値だからです。

// count が 0 のときに押すと…
setCount(count + 1); // setCount(0 + 1) → 1 にして、と依頼
setCount(count + 1); // setCount(0 + 1) → やっぱり 1 にして
setCount(count + 1); // setCount(0 + 1) → やっぱり 1 にして

3 回とも「1 にしてください」と頼んでいます。 結果が 1 になるのは当然です。

snapshot.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function Snapshot() {
  const [count, setCount] = useState(0);

  const addThree = () => {
    // 3 回呼んでいるが、この関数の中では count はずっと同じ値のまま。
    // 3 回とも「0 + 1」を依頼していることになる
    setCount(count + 1);
    setCount(count + 1);
    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={addThree}>
        3 増やすつもり
      </Button>
      <Button size="sm" variant="outline" onClick={() => setCount(0)}>
        リセット
      </Button>
    </div>
  );
}

state は「その回の写真」

前の章でやったとおり、コンポーネントは state が変わるたびに実行し直されます。

そして1 回の実行の間、state の値は変わりません。 その実行が始まった時点の値のまま、最後まで固定されています。 撮った写真が途中で変化しないのと同じです。

setCount は「次はこの値で実行してください」という予約です。いま動いている実行には影響しません。

snapshot.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function Snapshot() {
  const [count, setCount] = useState(0);

  const addThree = () => {
    // 3 回呼んでいるが、この関数の中では count はずっと同じ値のまま。
    // 3 回とも「0 + 1」を依頼していることになる
    setCount(count + 1);
    setCount(count + 1);
    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={addThree}>
        3 増やすつもり
      </Button>
      <Button size="sm" variant="outline" onClick={() => setCount(0)}>
        リセット
      </Button>
    </div>
  );
}

いまの値を使いたいときは関数を渡す

「1 増やす」のようにいまの値をもとに決めたいときは、 値ではなく関数を渡します。

setCount((current) => current + 1);

こう書くと、React は更新するときにそのときの最新の値current に入れて呼びます。 自分で値を計算しないので、古い値を使ってしまう心配がありません。

関数を渡す形直した例

今度はちゃんと 3 増える

0

React は依頼を順番に適用します。 0 に対して +1 して 1、その 1 に +1 して 2、その 2 に +1 して 3。積み上がるので、3 増えます。

どちらを使えばいいか

  • いまの値を使うなら関数を渡す(+1、切り替え、追加など)
  • 関係のない値を入れるなら、そのまま渡してよい(リセット、入力値の反映など)

迷ったら関数を渡す形にしておけば、まず間違いません。

updater.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function Updater() {
  const [count, setCount] = useState(0);

  const addThree = () => {
    // 値ではなく「いまの値をどう変えるか」を渡す。
    // React が順番に適用するので、3 回ぶんが積み上がる
    setCount((current) => current + 1);
    setCount((current) => current + 1);
    setCount((current) => current + 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={addThree}>
        3 増やす
      </Button>
      <Button size="sm" variant="outline" onClick={() => setCount(0)}>
        リセット
      </Button>
    </div>
  );
}

直後に読んでも変わっていない

const handleClick = () => {
  setCount(count + 1);
  console.log(count); // まだ古い値が出る
};

「更新に時間がかかっているのでは」と思うかもしれませんが、違います。この実行における count は、最初から最後まで同じ値だからです。 待っても変わりません。

新しい値を使いたい処理があるなら、 その値を変数に入れておくか、次の実行で扱います。

const next = count + 1;
setCount(next);
console.log(next); // これなら新しい値

snapshot.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function Snapshot() {
  const [count, setCount] = useState(0);

  const addThree = () => {
    // 3 回呼んでいるが、この関数の中では count はずっと同じ値のまま。
    // 3 回とも「0 + 1」を依頼していることになる
    setCount(count + 1);
    setCount(count + 1);
    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={addThree}>
        3 増やすつもり
      </Button>
      <Button size="sm" variant="outline" onClick={() => setCount(0)}>
        リセット
      </Button>
    </div>
  );
}

理解できたか確かめる

確認クイズ

setCount(count + 1) を 3 回並べると、count はいくつ増える?

確認クイズ

setCount((c) => c + 1) と書く利点は?

updater.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function Updater() {
  const [count, setCount] = useState(0);

  const addThree = () => {
    // 値ではなく「いまの値をどう変えるか」を渡す。
    // React が順番に適用するので、3 回ぶんが積み上がる
    setCount((current) => current + 1);
    setCount((current) => current + 1);
    setCount((current) => current + 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={addThree}>
        3 増やす
      </Button>
      <Button size="sm" variant="outline" onClick={() => setCount(0)}>
        リセット
      </Button>
    </div>
  );
}

この章のまとめ

  • 1 回の実行の間、state の値は固定されている
  • だから setCount の直後に読んでも古い値のまま
  • 値を渡す形で複数回呼んでも積み上がらない
  • いまの値をもとに決めたいときはsetCount((c) => c + 1)関数を渡す

updater.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function Updater() {
  const [count, setCount] = useState(0);

  const addThree = () => {
    // 値ではなく「いまの値をどう変えるか」を渡す。
    // React が順番に適用するので、3 回ぶんが積み上がる
    setCount((current) => current + 1);
    setCount((current) => current + 1);
    setCount((current) => current + 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={addThree}>
        3 増やす
      </Button>
      <Button size="sm" variant="outline" onClick={() => setCount(0)}>
        リセット
      </Button>
    </div>
  );
}