【入門編】 関数型更新による競合状態の解決 – React実践ガイド

こんにちは!Reactの学習、楽しく進めていますか?
画面がスイスイ動くWebアプリを作れるようになってくると本当にワクワクしますよね。

でも、Reactを触り始めて少し経った頃、誰もが一度は「あれ?なんで今の値が反映されないんだろう…?」という不思議な現象にぶつかります。ボタンを素早く連打したときに、カウントがうまく増えてくれなかったり、入力したデータがなぜか消えちゃったりするあの現象です。

今回は、そんなReactの「あるあるな罠」をスマートに回避する、とっても大切なテクニック「関数型更新(かんすうがたこうしん)」について、おしゃべりするようにお話ししていきますね。

難しい専門用語はなるべく使わずに、身近な例え話から紐解いていきますので、コーヒーでも飲みながらリラックスして読んでいってください。

—

1. なぜ「連続クリック」で計算が狂うのか?(お買い物のレジの列で例えてみる)

まずは、Reactの `useState` が普段裏側でどんな風に働いているのか、イメージしてみましょう。

例えば、通販サイトの「お買い物カゴ」をイメージしてください。いま、カゴの中に商品が「0個」入っています。ここに、あなたが「追加ボタン」をものすごい速さで2回連続でポチポチッ!と押したとします。

あなたの頭の中では、「0個」+「1個」+「1個」で、最終的には「2個」になっているはずですよね。

ところが、普通の `useState` を使ったコードでこれをやると、「1個」で止まってしまうことがあるんです。えっ、なんで?って思いますよね。

Reactの更新は「注文のまとめ出し」方式

Reactの `useState` で状態を更新するとき、それは「その場で即座にメモ書きを書き換える」のではなく、「次の休み時間(再描画のタイミング)に、この新しい値に書き換えておいてね!」という『お願い(予約)』をホワイトボードに貼る作業に似ています。

先ほどの連続ポチポチの裏側では、こんなことが起きています。

1. 1回目のクリック:
「今の値は `0` だから、次は `0 + 1` で `1` にしてください!」というお願いを貼る。
2. 2回目のクリック(1回目の結果がまだ反映される前!):
「今の値は `0` だから、次は `0 + 1` で `1` にしてください!」というお願いをもう一枚貼る。

……お気づきでしょうか?
2回目のクリックをした瞬間、まだ画面は書き換わっていないので、Reactは「今の値はまだ0だ」と思い込んでいるんです。その結果、「0に1を足す」という古い記憶に基づいたお願いが2枚並んでしまい、何度ボタンを押しても「1」にしかならないという悲劇が起きます。

これが、状態管理の現場でよくある競合状態(レースコンディション)の正体です。

—

2. 救世主!「関数型更新」というスマートな解決策

この「古い記憶のままお願いしちゃう問題」をスパッと解決してくれるのが、関数型更新という書き方です。

言葉にすると難しそうですが、やり方はとってもシンプル。`setCount` の中身に、直接「次の数字」を渡すのではなく、「直前の最新の状態を受け取って、計算して返す関数」を渡してあげるだけです。

書き方の違いを見てみよう

これまで(ちょっと危うい書き方):

// 「今の count の値」をそのまま使っちゃうので、連続技に弱い
setCount(count + 1);

これから(安心・安全な関数型更新):

// Reactくんに「直前の最新の値をちょうだい!それを元に計算するから!」とお願いする
setCount((prevCount) => {
return prevCount + 1;
});

この `prevCount`(名前は `current` でも `val` でも何でもOKです)には、Reactがその瞬間に把握している本当の最新の状態がちゃん入ってきます。

先ほどのお買い物カゴの例で言うと、
「レジの担当さん、いまカゴに入っている最新の個数を教えて!そこに1を足して!」
と、その都度バトンタッチしながら計算するようなイメージです。これなら、どれだけ素早く連打されても計算が狂うことはありません!

—

3. 実践!コードで動きを確認してみよう

それでは、実際に動かせるコードを見てみましょう。
今回は、よくある「カウントアップ&連続で3回増やすボタン」を例に、普通の書き方と関数型更新の違いを体験してみます。

import React, { useState } from ‘react’;

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

// 【危うい書き方】連続で状態を更新しようとするパターン
const handleBadIncrement = () => {
// ここで count を3回連続で増やそうとしても、
// すべて「今の count の値」を基準にしてしまうため、1回分しか増えません…!
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};

// 【安心な書き方(関数型更新)】
const handleGoodIncrement = () => {
// Reactが最新の状態をバトンリレー形式で引き継いでくれるため、確実に「+3」されます!
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};

return (

現在のカウント: {count}

);
}

「連続で3回増やす(危うい版)」のボタンをポチッと押してみてください。カウントは「3」ではなく「1」しか増えなかったはずです。
次に「リセット」を押してから、「連続で3回増やす(関数型更新版)」を押してみてください。こちらは狙い通り「3」一気に増えますよね!

この小さな違いが、複雑なWebアプリを作るときにバグを防ぐ大きな分かれ道になります。

—

4. チーフアーキテクトからのアドバイス:いつ関数型更新を使うべき?

「じゃあ、これからは全部の `useState` であの長い書き方をしなきゃいけないの……?」

そんなことはありません!すべてを関数型更新にする必要はないので安心してください。現場のプロたちがどんな基準で使い分けているか、こっそり教えちゃいますね。

① 単純なトグルや、新しい値をドカンと上書きするとき

例:モーダルの開閉(`setIsOpen(true)`)や、入力フォームの文字を丸ごと入れ替えるとき
これらは「直前の値がいくつだったか」に依存しないため、これまでの書き方(`setIsOpen(!isOpen)` なども簡単なものならOKですが、次の状態に依存するなら関数型が安全です)で全く問題ありません。

② 「直前の値」を計算のベースにするとき

例:カウンター、リストへの要素の追加・削除、ショッピングカートの数量変更、非同期処理(`async/await`)のなかで状態を更新するとき
ここは迷わず関数型更新を使いましょう! 特に、API通信の応答を待っている間など、時間が経ってから状態を更新する場合は、古い状態を参照してしまうリスクが跳ね上がるため、関数型更新がマストな防衛策になります。

—

おわりに:少しずつ、確実に身につけていけば大丈夫

最初は「え、書き方がちょっと長くなって面倒だな…」と感じるかもしれませんが、これはReactという優秀な相棒と長く付き合っていくための、とっても優しくて頼もしいお作法です。

もし実務や個人開発で「あれ、なんか状態の更新がおかしいぞ?」とハマったら、このバトンリレーのイメージ(関数型更新)を思い出してみてくださいね。

あなたのReactライフが、よりスムーズで楽しいものになりますように。
それでは、また次の記事でお会いしましょう!バグに負けず、ハッピーなコーディングを!

コメント

タイトルとURLをコピーしました