1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

setStateは次のレンダー用に値を予約するだけ

1
Posted at

Reactで誰もがやるやつ。

import { useState } from "react";

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

  const handleClick = () => {
    setCount(count + 1);
    console.log("setStateの直後:", count);     // 更新後の値が見たい
  };

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

ボタン1回。結果。

setStateの直後: 0

count: 1setCount を呼んだのに、直後の count は変わってない。

すぐに変わるわけじゃない setState

setCount(count + 1)count を書き換える命令ではない。 「次の render では count1 にしてくれ」と React に予約するだけ。今動いている関数の中の count はその render 時点の snapshot なので、予約しても書き換わらない。

useState宣言はこう。

const [count, setCount] = useState(0);

左辺の [count, setCount] は配列ではなく分割代入。useState が返す配列 [0, 関数] を分解して、0番目を count、1番目を setCount に入れているだけ。だから count の中身はただの const 数値 0

数値を const で束縛したら、この関数の実行中に変わりようがない。JavaScriptとして当たり前。

count は「今の状態を指す変数」ではなく 「その render 専用の定数」setCount は次の render 用に値を予約するだけ。

新しい値を使いたいときは

更新後の値を今すぐ使いたいときは?

1. 自分で計算した値をそのまま使う

setCount(count + 1) を呼ぶ時点で、count + 1 は自分で計算している。だったらその値を変数に持てばいい。

const handleClick = () => {
  const next = count + 1;
  setCount(next);
  console.log("次のcount:", next);
};
次のcount: 1

setCount のあとに count を読もう」ではなく「次の値は自分で持っておく」。

2. setState に関数を渡して更新する

もう一つは setCount(prev => prev + 1)

値ではなく「今の値を受け取って次の値を返す関数」を渡す。Reactが最新のstateを引数に渡してくれる

prev はどこかで宣言済みの変数ではない。この場で作った関数の引数名。分解するとこう。

const updater = (prev) => prev + 1; // 前の値を受け取って +1 を返すだけの関数
setCount(updater);                  // 値ではなく関数を渡す

この関数を呼ぶのは自分ではなく React。予約を消化するとき その時点の最新 state を prev に入れて呼んでくれる 。引数名なので prev でも c でも何でもいい。

違いは setCount を連続で呼んだとき。値をそのまま入れると:

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};

3増えると思きや1しか増えない。

count: 1

理由。3回の setCount(count + 1) はどれも同じ count(= 0)を参照している。0 + 1 を3回予約しているだけ。だから 1

関数形式で同じことを試す。

const handleClick = () => {
  setCount(prev => prev + 1);  // prev = 0 -> 1 になる
  setCount(prev => prev + 1);  // prev = 1 -> 2 になる
  setCount(prev => prev + 1);  // prev = 2 -> 3 になる
};
count: 3

こっちは3増える。関数形式は 前回の予約結果prev として受け取れる。

つまり関数形式が要るのは、直前のstateに依存して次のstateを決めるとき。単純な代入(setName("bob")など)ならどっちでも結果は変わらない。

本当に「更新後の値」を見たいとき

state更新後の状態で何かしたいなら useEffect で拾う

import { useEffect, useState } from "react";

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

  useEffect(() => {
    console.log("countが変わった:", count);
  }, [count]);

  return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}

ボタン2回。

countが変わった: 0
countが変わった: 1
countが変わった: 2

useEffect はその処理を render 後に走らせる仕組み

React の1回の render は「Render phase(component 関数を呼んで次の画面を計算する)」と「Commit phase(計算結果を DOM に反映する)」に分かれている。useEffect の処理が走るのは Commit のさらに後(Passive Effects)。この粒度で並べるとこう。

分割のポイント。count1 に確定するのは「re-render が始まる前」ではなく、Render phase で useState(0) が呼ばれた瞬間。ここでuseStateはそれまでに queue されていた予約を実行して count を更新する。
useState(0)0 を入れているのに、これはあくまで default valueなだけで、その前のrenderで count の値の変更が予約されているため default value 0 は使われない。だから component 関数の実行中に count の値が動くことはなく、各 render の中では常に定数。

setCount の直後に何かしたい」は、たいてい「stateが更新されたら何かしたい」に言い換えられる。前者は無理。後者は useEffect で書ける。

補足:境界

  • ここで書いた挙動は、少なくとも確認した範囲(React 18 / useState)でのもの。React 17以前と18以降では event handler 外の batching 挙動が違う。
  • useRef は逆に「その場で書き換わる」入れ物。ただし変えても re-render は起きない。値は保持したいが画面には関係ない、というケース向け。
  • 上の説明はClaude Opus 4.8に教わった内容を、自分で動かして確認したもの。仕様の一次情報はReact公式ドキュメント。記事末尾にリンク。

わかったこと

setState は「今の値を変える」命令ではなく 「次の render 用に値を予約する」命令

  • setCount(x) はその場で count を書き換えない。次の render 用の予約。
  • countが変わるのは次のrender時。何度 setCount を呼んでも re-render は次の1回に 畳み込まれるsetCount を3回ならその3回を順にやった後に次のrenderが走る)
  • 今の render 中の count はその render 専用の snapshot。あとから読んでも古い。 const ということを忘れない

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?