【入門編】 関数型更新(Updater Function)の仕組み – React実践ガイド

こんにちは!Reactの学習、楽しく進められていますか?
画面がスイスイ動く魔法のようなアプリが作れる一方で、最初は「あれ?なんだか思っていた通りにデータが更新されないぞ……?」と首をかしげる瞬間、ありますよね。

大丈夫ですよ、その戸惑いは誰もが通る「Reactの洗礼」のようなものです。あなただけではありません。

今日は、そんなReact初心者の心をゴシゴシと癒やしてくれる、状態管理の超重要テクニック「関数型更新(Updater Function)」について、身近なお買い物の例えを交えながら、優しく、そして深く紐解いていきたいと思います。

—

なぜ、あのバグは起きるのか?(お買い物のレジ待ちでおこる悲劇)

Reactでデータを保存する基本といえば、お馴染みの `useState` ですよね。
例えば、ボタンを押すたびに数字が増える「カウンター」を作るとき、こんな風にコードを書いたことはありませんか?

// カウントの数値を保持する状態(初期値は0)
const [count, setCount] = useState(0);

// ボタンがクリックされたときの処理
const handleIncrement = () => {
setCount(count + 1);
};

「今の `count` の値に `1` を足してセットする」。直感的で、なんの問題もなさそうに見えます。
実際、ボタンを1回ずつポチポチ押す分には、これでもちゃんと動きます。

ですが、ここに「ちょっと意地悪な状況」を想像してみてください。
あなたはスーパーのレジに並んでいます。カゴの中に「りんご」が1つ入っています。

1. あなたが「もう1個りんごを追加して!」と店員さんにお願いしました(1回目の `setCount(count + 1)`)。
2. 店員さんが奥からりんごを取りに行っているまさにその瞬間、後ろに並んでいたお友達が、あなたに内緒で「さらにもう1個追加して!」と別の店員さんにお願いしました(2回目の `setCount(count + 1)`)。

さて、店員さんが戻ってきたとき、カゴの中身はどうなっているべきでしょうか?
正解は「最初にあった1個」+「あなたがついかした1個」+「お友達が追加した1個」で、合計3個ですよね。

しかし、Reactの世界で普通の書き方をしていると、こんなことが起きます。
「いま手元にある伝票(最初に見聞きした `count = 0`)によると、追加は1個だから……はい、合計1個!」と、お友達の追加がなかったことにされてしまうのです。

これが、Reactの `useState` が持つ「状態更新の非同期バグ(古い状態を参照してしまう問題)」の正体です。
Reactは、パフォーマンスを最大化するために、画面の描き換え(レンダリング)を効率よくまとめようとします。そのため、短い時間に何度も `setCount` が呼ばれると、まだ古い記憶(古い `count` の値)を持ったまま計算が行われてしまい、データが消えてしまうことがあるのです。

—

救世主、「関数型更新」の登場!

この「古い記憶のまま計算しちゃう問題」をスパッと解決してくれるのが、今回主役の「関数型更新(Updater Function)」です!

難しく考える必要はありません。
通常の `setCount(count + 1)` の代わりに、「現在の最新の状態を必ず受け取る魔法の関数」を `setCount` の中にスポッと放り込んであげるだけです。

書き方を比べてみましょう。

// 【これまで】直接値を渡す(古いデータを掴んじゃうリスクがある)
setCount(count + 1);

// 【これからの新しい私】関数を渡す!
setCount((prevCount) => {
return prevCount + 1;
});

この `(prevCount) => { return prevCount + 1; }` という部分が、関数型更新です。
引数にある `prevCount`(名前は `current` でも `state` でも何でもOKです)には、「今まさにReactが把握している、正真正銘の最新のデータ」が自動的にスポッと入ってきます。

先ほどのスーパーの例えで言うなら、
「伝票をパッと見るな! 今まさにレジのカゴの中に入っている現実の個数をその場で数えてから、そこに1を足しなさい!」
と店員さんに厳命するようなものです。これなら、お友達がこっそり追加した分を見落とすこともありませんよね。

—

実践!コードでその動きを確かめてみよう

百聞は一見にしかず。実際に動かせるコンポーネントのコードを見てみましょう。
今回は、ボタンを「一瞬で2回連続して押す(カウントを2増やす)」というちょっと意地悪な操作をするシチュエーションを考えてみます。

import React, { useState } from ‘react’;

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

// 【バグが起きやすい書き方】
// ボタンを1回押すと「+2」したいけれど……?
const handleBadDoubleIncrement = () => {
// ここで count はまだ「古い値」を参照しているため、
// 2行書いても、実質「0 + 1」が2回起きるだけで、結果は「1」になってしまいます。
setCount(count + 1);
setCount(count + 1);
};

// 【安心・安全な関数型更新の書き方】
// 最新の状態をバトンタッチしながら確実に増やしていく!
const handleGoodDoubleIncrement = () => {
// 1回目の更新:最新の count を受け取り、+1 する
setCount((prevCount) => prevCount + 1);

// 2回目の更新:1回目で計算された「最新の数」を確実に受け取り、さらに +1 する
setCount((prevCount) => prevCount + 1);
};

return (

);
}

「危ないボタン」を押すと、2回 `setCount` を呼んでいるのに、なぜか1しか増えないもどかしさを体験できます。
一方、「安心ボタン」を押すと、ちゃんと狙い通り「2」増えてくれますよね。これが、私たちが関数型更新を愛する理由です。

—

いつ、どんなときに「関数型更新」を使うべき?

「じゃあ、これからは全部関数型更新で書かなきゃダメなの?」と思われるかもしれませんが、そんなことはありません。使い分けのコツはとてもシンプルです。

1. 「今の状態(前回の値)」をベースにして次の状態を計算するとき

  • カウンターの増減(`+1` や `-1`)
  • リストに新しいアイテムを1件追加する(`[…prevList, newItem]`)
  • チェックボックスのオンオフを切り替える(`!prevChecked`)

👉 こういうときは迷わず「関数型更新」を使いましょう!

2. 「前の状態とは全く関係なく、新しい値をドンと上書きする」とき

  • ユーザーがテキスト入力欄に打ち込んだ文字をそのまま保存する(`setText(e.target.value)`)
  • 画面のタブを「プロフィール画面」に強制的に切り替える(`setActiveTab(‘profile’)`)

👉 ここは直接値を渡しても大丈夫です。

迷ったときは、「あ、これって前の値にちょっと手を加えるやつだな」と思ったら、とりあえず `(prev) => prev …` の形を思い出してみてください。それだけで、不可解なバグの多くを未然に防ぐことができます。

—

おわりに

Reactの状態管理は、最初はパズルのように難しく感じることも多いです。
でも、仕組みの裏側にある「なぜそうするのか」という理由(お買い物のレジの例えのように!)が分かると、コードを書くのがぐっと楽しく、誇らしくなってきます。

今日からあなたのコードに「関数型更新」を取り入れて、より堅牢でバグ知らずなReactライフを楽しんでくださいね。
あなたのフロントエンドの旅を、心から応援しています!

コメント

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