【入門編】 reducer関数における純粋関数としての実装ルール – React実践ガイド

こんにちは!Reactの学習、楽しく進めていますか?
画面を思い通りに動かせるようになってくると本当にワクワクしますが、同時に「あれ? 画面の値が変わらない…?」「さっきまで動いていたのに、なぜかバグる…」なんて壁にぶつかることも、この世界では日常茶飯事です。大丈夫、みんな最初はそこで盛大につまづきますから、安心してくださいね。

さて、今回はReactのState(状態)管理のなかでも、少しステップアップした強力な相棒である「useReducer」、そしてその心臓部である「reducer関数」についてお話しします。

なんだか「Reducer(リデューサー)」なんて専門用語を聞くだけで、頭から煙が出そうになるかもしれません。でも、身近な例えを交えながら優しくほどいていくので、コーヒーでも飲みながらリラックスして読んでいってくださいね。

—

1. なぜReducerが必要になるの?(お買い物のレジを想像してみよう)

Reactの基本である `useState` は、シンプルで本当に使いやすいですよね。「カウントを1増やす」「モーダルを開く・閉じる」といった簡単な状態管理なら `useState` で十分お釣りが来ます。

しかし、アプリが大きくなってくるとどうでしょう。
例えば、オンラインショップの「お買い物カゴ」画面を想像してみてください。

  • 商品の数量を増やす
  • セールクーポンを適用する
  • 配送方法を変える
  • ギフトラッピングの希望にチェックを入れる

これらが複雑に絡み合い、あっちの処理でもこっちの処理でも `useState` で状態をバラバラに更新していると、「一体、今のカゴの中身はどうなっているんだっけ?」と管理しきれなくなってしまいます。

ここで登場するのが `useReducer` です。
イメージとしては、「お買い物カゴを管理する、専属の優秀なレジ係」を一人雇うようなもの。

あなた(コンポーネント)は、レジ係に向かって「この商品を1つ追加して!」「クーポンコードを入れて!」という「注文票(アクション)」を渡すだけ。レジ係は、その注文票を見て、現在のカゴの中身と照らし合わせながら、正確に新しいカゴの中身を計算してくれます。

この「注文票をもとに、新しい状態を計算する専用のルールブック」こそが、今回フォーカスする `reducer関数` なのです。

—

2. reducer関数における「絶対のルール」:純粋関数とは?

さて、この優秀なレジ係(reducer関数)には、絶対に守らなければならないたった一つの鉄の掟があります。

それが、「純粋関数(Pure Function)として実装しなければならない」ということです。

なんだか硬い言葉ですね。でも、要する

1. 同じ注文票(アクション)と、同じ現在の状態を渡されたら、100%確実に同じ結果を返すこと。
2. 関数の中で、外の世界(画面の外、データベース、API通信、現在時刻など)を勝手に書き換えたり、影響を与えたりしないこと。

というシンプルな約束事です。

身近な例え:自動販売機とガチャガチャ

例えば、あなたが自動販売機に100円を入れて「お茶」のボタンを押したら、絶対に「お茶」が出てきますよね。もしボタンを押した時の「気分」や「天候」によって、ときどきハンバーグが出てきたり、何も出てこなかったりしたら……怖くてそんな自動販売機使えませんよね。

レジ係(reducer)もこれと同じです。
「今の状態:カゴに商品が1個」のときに、「追加注文」のチケットを渡されたなら、何度やり直しても「カゴに商品が2個」という結果を寸分狂わず返す必要があります。

—

3. やってはいけない「禁断の行為」(イミュータビリティの破壊)

初学者がreducer関数を書くとき、一番やってしまいがちで、かつ最も恐ろしいバグが 「既存の状態(State)を直接いじってしまうこと」 です。

プログラミングの世界では、データは「変更不可(イミュータブル:Immutable)」であるべきだと言われます。

❌ やってはいけない「直接書き換え(ミューテーション)」の例

// 【悪い例】絶対にやっちゃダメな書き方!
function badReducer(state, action) {
switch (action.type) {
case ‘ADD_ITEM’:
// ⚠️ 既存の配列を push で直接いじっちゃってる!
state.items.push(action.payload);
return state; // 書き換わった同じ state をそのまま返している

default:
return state;
}
}

これの何がいけないのでしょうか?
Reactは、「おっ、Stateの参照先が変わったから、画面を再描画(レンダリング)しなきゃ!」と、新旧のデータの「箱」が変わったかどうかで判断しています。

上の悪い例では、箱(配列)の中身をグリグリと直接書き換えただけで、箱そのものは同じものを使っています。そのため、Reactは「あれ? 状態は変わってないみたいだから、画面の書き換えはパスしようっと」と勘違いしてしまい、操作したのに画面が更新されない(あるいはデバッグが異常に難しくなる)という、悪夢のようなバグを引き起こすのです。

⭕ 正しい「新しい状態を作り出す」書き方

正解は、「今の状態をベースにして、中身がコピーされた新しい状態の箱をまるごと新しく作り直して返す」ことです。

// 【良い例】これぞプロの作法!
function goodReducer(state, action) {
switch (action.type) {
case ‘ADD_ITEM’:
return {
…state, // スプレッド構文で、これまでの状態をパッとコピーして
items: […state.items, action.payload] // アイテムの入った「新しい配列」に差し替える!
};

default:
return state;
}
}

スプレッド構文(`…`)を使うことで、「元のデータを汚さずに、新しいデータを作る」という安全な操作(イミュータブルな更新)が簡単にできます。

—

4. 実戦!ショッピングカートで学ぶreducer実装

それでは、ここまでのお話をふまえて、実際にそのままコピペして動かせるサンプルコードを見てみましょう。
コメントを丁寧に書いているので、エディタに貼り付けて挙動を確かめてみてくださいね。

import React, { useReducer } from ‘react’;

// 1. 最初のお買い物カゴの状態(初期値)を定義します
const initialState = {
items: [‘りんご’, ‘みかん’],
totalCount: 2,
};

// 2. 状態を計算する「レジ係」こと reducer関数です
// ルール:(現在の状態, 注文票) => 新しい状態
function shoppingReducer(state, action) {
// 注文票の「種類 (type)」によって処理を分岐します
switch (action.type) {
case ‘ADD_ITEM’:
return {
…state, // 一旦これまでの状態を展開して保持
items: […state.items, action.payload], // 新しい商品を足した「新しい配列」を作る
totalCount: state.totalCount + 1, // 合計数もきっちり増やす
};

case ‘CLEAR_CART’:
// カゴを空にするアクション
return {
…state,
items: [],
totalCount: 0,
};

default:
// 想定外の注文票が来たときは、とりあえず今の状態をそのまま返す(お約束)
return state;
}
}

export default function ShoppingCartApp() {
// useReducerフックを呼び出します
// state: 現在のカゴの状態, dispatch: レジ係に注文票を渡すための関数
const [state, dispatch] = useReducer(shoppingReducer, initialState);

// ボタンが押されたときに実行される関数たち
const handleAddBanana = () => {
// dispatchに「注文票(アクションオブジェクト)」を乗せてレジ係に渡す
dispatch({ type: ‘ADD_ITEM’, payload: ‘バナナ’ });
};

const handleClear = () => {
dispatch({ type: ‘CLEAR_CART’ });
};

return (

🛒 私のお買い物カゴ

現在の商品の数: {state.totalCount}個

    {state.items.map((item, index) => (

  • {item}
  • ))}

{/ アクションを発火(ディスパッチ)するボタン群 /}

);
}

—

5. チーフアーキテクトからのエール

お疲れ様でした!ここまで読んでいただきありがとうございます。

reducer関数における「純粋性(Immutability)」の概念、なんとなくイメージできたでしょうか?

最初は「なんでわざわざコピーを作らなきゃいけないんだろう? 直接書き換えちゃった方が楽なのに!」と感じるかもしれません。実際、私も駆け出しの頃は何度も直接書き換えては、画面が更新されずに頭を抱えたものです。

ですが、この「データを直接いじらず、新しいデータとして返す」というルールを守ることで、「アプリの動きが圧倒的に予測しやすくなり、バグの温床を綺麗に排除できる」という強力なメリットを手に入れることができます。

Reactの世界では、この「純粋性」を意識することが、ワンランク上のエンジニアになるための大きな一歩になります。

もし今日書いてみたコードで分からないところがあったり、「ここはどういうこと?」という疑問が湧いたら、いつでも気軽に周りの先輩やコミュニティに頼ってくださいね。一歩ずつ、確実に前に進んでいきましょう。応援しています!

コメント

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