3
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?

setCount した直後の count は、まだ更新前の値です

3
Posted at

AIに「ボタンを押したらカウントを増やして、その値をログに出して」と頼むと、こういうコードが返ってくることがあります。

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

const handleClick = () => {
  setCount(count + 1);
  console.log(count);   // ← 増える前の値が出る
};

動かすと、画面の数字はちゃんと増えます。でもログには1つ前の値が出ます。1回目のクリックでは 0、2回目では 1、3回目では 2。画面は 1 → 2 → 3 と増えているのに、ログだけが常に1つ前の値になります。

この記事は、なぜこうなるのかと、AIに直させるときに何を伝えると一発で通るかの話です。依存配列の話ではなく、更新を予約した直後に値を読む話です。

誤解その1:「setCount は変数に代入している」

まず、ここが誤解の入口です。setCount(count + 1) は count という変数を書き換えているわけではありません。

count は useState が返したこのレンダリング用に作られた値です。const で受けているとおり、あとから書き換わることはありません。

setCount がやっているのは、「次のレンダリングではこの値にしてください」という予約です。予約した直後に count を読んでも、そのハンドラが作られたときのレンダリングの値がそのまま返ってきます。

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  // count が 0 のとき、3回とも「次は 1 にして」と予約している
  // → 結果は 3 ではなく 1
};

3回呼んでも1しか増えないのは、3回とも同じ count(=0)を見て 0 + 1 を計算しているからです。

誤解その2:「await すれば新しい値が読める」

「非同期だから待てばいい」と考えて、こういう修正が出てくることがあります。

const handleClick = async () => {
  setCount(count + 1);
  await new Promise((r) => setTimeout(r, 0));
  console.log(count);   // ← やっぱり古い値
};

これも古い値のままです。理由は「まだ更新が終わっていないから」ではなく、count がこのレンダリング時点の定数として関数の中に閉じ込められているからです。時間が経っても、この関数が見ている count は変わりません。

setCount を呼んだ時点で、更新の予約自体はすぐ積まれます。それでも古い値が出るのは、新しい値が次のレンダリングで作り直された別の count として現れるからです。同じ関数の中で待っても出てきません。

直し方1:更新後の値が欲しいなら、自分で計算する

ログに出したいだけなら、これで足ります。

const handleClick = () => {
  const next = count + 1;
  setCount(next);
  console.log(next);    // 更新後の値
};

当たり前に見えますが、「stateから読む」のではなく「自分で作った値を使う」に発想を変えるのがポイントです。

なお、これは1クリックで1回だけ更新する場合の書き方です。連続して複数回更新が走るなら、次の関数形式が必要になります。

直し方2:前の値を元に更新するなら、関数形式を使う

連続で呼ぶ場合や、非同期処理を挟む場合は、こちらが必要になります。

const handleClick = () => {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  // → 3 増える
};

setCount に関数を渡すと、React が直前までの更新をすべて反映した状態を prev として渡してくれます。外側の count を参照しないので、閉じ込められた古い値の影響を受けません。

判断基準はシンプルで、新しい値が「前の値」に依存するなら関数形式です。

setCount(prev => prev + 1);        // 前の値に依存する → 関数形式
setCount(0);                        // 依存しない → そのままでよい
setUser({ name: '田中' });          // 依存しない → そのままでよい(他の項目がない前提)
setUser(prev => ({ ...prev, name: '田中' }));  // 既存の値を残したい → 関数形式

直し方3:更新後に何かしたいなら useEffect

依存配列そのものの罠(無限ループ/更新されない)は以前まとめたので、ここでは「更新後に動かす置き場所」としてだけ触れます。

「値が変わったあとにAPIへ送りたい」のような場合は、更新を待つのではなく、変わったことを検知して動く形にします。

useEffect(() => {
  console.log('count が変わりました:', count);
}, [count]);

handleClick の中で頑張って待とうとするのではなく、変わったことを検知して動かす形にできます。

ただし、この useEffect は最初のレンダリングでも1回走ります。ログだけなら気になりませんが、ここにAPI送信を書くと、まだ何も操作していない状態で1回送られます。「そのクリックのときだけ動かしたい処理」なら、ハンドラ側(直し方1)に書くほうが素直です。どうしても変化を検知して動かしたい場合だけこの形にして、初回を飛ばす判定を入れてください。React 18 以降の開発モード(StrictMode)では、初回のエフェクトが2回実行される点にも注意です。

AIに直させるときの頼み方

「ログに古い値が出ます」とだけ伝えると、setTimeout で待つ修正や、useRef で値を持ち回る修正が返ってくることがあります。ただし setTimeout で待つ形は、誤解その2と同じ理由でそもそも直りません(コールバックが見ている count も、同じレンダリングの値のままです)。useRef は「自分で ref.current に代入したあと」なら最新の値として読めます。ただし useEffect で同期する形だと、代入が走るのは再レンダリングのあとなので、そのハンドラの中ではやはり古い値のままです。多くの場合は直し方1で足ります。

前提を添えると変わります。

React で、setCount の直後に count をログに出すと古い値が出ます。
以下の前提で修正してください。

- state の更新は次のレンダリングに反映される、という理解で合っていますか
- 「更新後の値をその場で使いたいだけ」なのか
  「前の値を元に更新したい」のかで、直し方が変わると思っています。
  今回は◯◯です
- setTimeout で待つ形は避けたいです
- useRef を新しく足す前に、それが必要かどうかを説明してください

修正後、なぜその形になるのかも1〜2行で書いてください。

ポイントは3つです。

  • 自分の理解を先に書いて、合っているか聞く(間違っていれば訂正が返ってくる)
  • やりたいことがどちらなのかを明示する(直し方が分岐するため)
  • 避けたい手段を先に伝える(setTimeout で待っても、同じレンダリングの count を見続けるので直りません)

最後の「なぜその形になるのか」を毎回書かせておくと、同じ質問を繰り返しにくくなります。

まとめ

  • count はそのレンダリング時点の定数。setCount は次のレンダリングへの予約
  • だから直後に読んでも古い。await しても同じ関数内では変わらない
  • 更新後の値を使いたいだけ → 自分で計算した値を使う
  • 前の値に依存する更新 → setCount(prev => ...) の関数形式
  • 更新後に処理したい → useEffect の依存配列に入れる
  • AIに頼むときは「更新後の値が欲しい」のか「前の値を元に更新したい」のかを先に伝える

未経験者向けの講座を運営しています

未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全20セッションで体系化した教材です。Claude Code を学習パートナーにする CLAUDE.md と学習モード(learner / developer)の設計までセットで含みます。

※ この教材は Qiita 読者の方には易しすぎる内容なので、初心者の知り合いへの紹介や社内研修の参考としてどうぞ。

3
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
3
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?