本文へスキップ
state は「その瞬間の写真」

Part 4 · 状態 (state)

27 / 64

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

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

  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 を 3 回でした。 では、別々の state を 3 つ更新したらどうなるでしょうか。 3 回描き直されそうに思えます。

const updateAll = () => {
  setName(...);
  setAge(...);
  setAdmin(...);
};
別々の state を 3 つ更新する

押すたびに render はいくつ増える?

{ name: "さとう", age: 20, admin: false }

1 しか増えません。3 つとも別の state なのに、描き直しは 1 回にまとめられました。 これをバッチングと呼びます。

もしまとめられなかったら、途中の状態が一瞬だけ画面に出てしまいます。 名前だけ変わって年齢は古いまま、という中途半端な画面です。 まとめることで、そういう状態を見せずに済んでいます。

待つ処理をはさんでも、まとめられる

もう 1 つのボタンは、await で 0.3 秒待ってから同じ 3 つを更新します。 こちらも render は 1 しか増えません

batching.tsx

"use client";

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

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

  const [name, setName] = useState("さとう");
  const [age, setAge] = useState(20);
  const [admin, setAdmin] = useState(false);

  // 3 つとも別々の state。それでも描き直しは 1 回にまとめられる
  const updateAll = () => {
    setName((current) => (current === "さとう" ? "すずき" : "さとう"));
    setAge((current) => current + 1);
    setAdmin((current) => !current);
  };

  // 待つ処理をはさんでも、まとめられる
  const updateAfterWait = async () => {
    await new Promise((resolve) => setTimeout(resolve, 300));
    updateAll();
  };

  return (
    <div className="flex flex-col gap-3">
      <div className="rounded-md border p-3 font-mono text-sm">
        {`{ name: "${name}", age: ${age}, admin: ${String(admin)} }`}
      </div>

      <div className="flex flex-wrap gap-2">
        <Button size="sm" onClick={updateAll}>
          3 つまとめて更新する
        </Button>
        <Button size="sm" variant="outline" onClick={updateAfterWait}>
          0.3 秒待ってから更新する
        </Button>
      </div>
    </div>
  );
}

state は「その回の写真」

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

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

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

snapshot.tsx

"use client";

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

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

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

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

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

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

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

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

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

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

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