useReducerの使い方

# はじめに
`useReducer` を使って同様のステート管理を行うと、より複雑なステートの更新ロジックを扱う場合に、コードが整理されやすくなります。特に、複数の関連するステート変更が絡み合う場合に有効です。
例えば、**ゲームに例えるとHPが一定以下になった時に能力が発動するような機能** が必要な時に重宝します。

`useReducer` を使ってカウンターの状態管理を行う例を紹介します。ここでは、カスタムフックとコンテキストを組み合わせて、複数のコンポーネントで同じステートを共有するパターンで `useReducer` を利用します。

# サンプル

```jsx
import React, { createContext, useContext, useReducer, useCallback } from 'react';

// アクションの型を定義
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// reducer関数
const counterReducer = (state, action) => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    default:
      return state;
  }
};

// カウンターのコンテキストを作成
const CounterContext = createContext(null);

// カウンターのステートとディスパッチ関数を提供するプロバイダー
const CounterProvider = ({ children }) => {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  const increment = useCallback(() => {
    dispatch({ type: INCREMENT });
  }, [dispatch]);

  const decrement = useCallback(() => {
    dispatch({ type: DECREMENT });
  }, [dispatch]);

  return (
    <CounterContext.Provider value={{ count: state.count, increment, decrement, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
};

// カウンターのステートと関数を利用するためのカスタムフック
const useCounterContext = () => {
  const context = useContext(CounterContext);
  if (!context) {
    throw new Error("useCounterContext must be used within a CounterProvider");
  }
  return context;
};

// カウンターを表示する子コンポーネント
const CounterDisplay = () => {
  const { count } = useCounterContext();
  return <div>現在のカウント: {count}</div>;
};

// カウントを増やすボタンを持つ子コンポーネント
const IncrementButton = () => {
  const { increment } = useCounterContext();
  return <button onClick={increment}>増やす</button>;
};

// カウントを減らすボタンを持つ子コンポーネント
const DecrementButton = () => {
  const { decrement } = useCounterContext();
  return <button onClick={decrement}>減らす</button>;
};

// 親コンポーネント
const ParentComponent = () => {
  return (
    <CounterProvider>
      <CounterDisplay />
      <IncrementButton />
      <DecrementButton />
    </CounterProvider>
  );
};

export default ParentComponent;
```

**変更点:**

1.  **`counterReducer` 関数:**
    * 現在のステートとアクションを受け取り、新しいステートを返します。
    * アクションの `type` に基づいて、どのようにステートを更新するかを定義します。
    * `INCREMENT` と `DECREMENT` のアクションを処理します。

2.  **`CounterProvider` コンポーネント:**
    * `useReducer` フックを使って、`state` と `dispatch` 関数を取得します。初期ステートは `{ count: 0 }` です。
    * `increment` 関数と `decrement` 関数は、それぞれ対応するタイプ (`INCREMENT`, `DECREMENT`) のアクションを `dispatch` 関数に渡します。`useCallback` を使用して、依存配列に `dispatch` を含めることで、`dispatch` 関数が変更された場合にのみ再生成されるように最適化しています。
    * コンテキストプロバイダーの値として、`state.count` と、アクションをディスパッチするための関数 (`increment`, `decrement`, `dispatch`) を提供します。

3.  **`useCounterContext` カスタムフック:**
    * コンテキストの値を取得し、それを返します。

4.  **子コンポーネント (`CounterDisplay`, `IncrementButton`, `DecrementButton`):**
    * `useCounterContext` フックを通じて、共有された `count` と `increment`, `decrement` 関数を利用します。

**`useReducer` を使うメリット:**

* **複雑なステート管理:** ステートの更新ロジックが複雑になるほど、`reducer` 関数内でロジックを集中管理できるため、コードの見通しが良くなります。
* **予測可能なステート遷移:** アクションを通じてのみステートが変更されるため、いつ、なぜステートが変更されたのかを追跡しやすくなります。
* **テストの容易性:** `reducer` 関数は純粋な関数であるため、入力（現在のステートとアクション）に対して常に同じ出力（新しいステート）が得られることを保証しやすく、単体テストが容易になります。

**`useReducer` を使うデメリット:**

* **ボイラープレートコードが増える:** シンプルなステート管理の場合でも、アクションの型定義や `reducer` 関数の記述が必要になるため、コード量が増えることがあります。

**どのような場合に `useReducer` を使うべきか:**

* ステートの更新ロジックが複数あり、相互に依存している場合。
* ステートの変更履歴を追跡したい場合（デバッグやUndo/Redo機能の実装など）。
* より予測可能でテストしやすいステート管理を目指したい場合。

今回のカウンターの例のように、単純なステートの増減だけであれば、`useState` でも十分にシンプルに実装できます。しかし、より複雑なステート管理が必要になった場合には、`useReducer` を検討することで、より堅牢で保守性の高いコードを書くことができるでしょう。