【状態管理の救世主】React の useReducer で複雑なコードにさようなら

## はじめに

恥ずかしながら、私はこれまで `useState`フックのみで状態管理を行ってきました。

しかし、`useReducer`フックなるものを知り、その便利さに驚きました。そこで、その利便性を共有するためにこの記事を書くことにしました。

`useReducer` を使うと、複数の関連する状態をひとまとめにして管理できるようになります。これは、複数の `useState` を使っていたときよりも整理されていて、扱いやすくなります。ぜひ使いこなせるようになりたいですよね。

この記事を読むことで、以下のことが理解できるようになります。

- **`useReducer` の基本的な使い方**
- **`useState` と `useReducer` の比較と使い分け方**
- **`useState` から `useReducer` への段階的な移行の手順**

以前の私と同じように状態管理の複雑さに悩む人の役に立てれば幸いです。
では、早速見ていきましょう！

## 1\. `useReducer` の基本的な使い方

まず、React から `useReducer` をインポートします。

```js
import { useReducer } from "react";
```

そして呼び出します。

:::note warn
他のフックと同様にコンポーネントのトップレベルもしくは独自のカスタムフックでのみ呼び出しが可能です。
:::

基本的なシグネチャは以下のようになります。

```js
const [ state, dispatch ] = useReducer(reducer, initialState);
```
それぞれの要素を説明していきます。

- **reducer**：状態とアクションを引数に取り、新しい状態を返す関数
- **initialState**：状態の初期値
- **state**：現在の状態を保持する変数
- **dispatch**：リデューサにアクションを与えてトリガーする関数

おそらく、ここで「？」が付くのは　**reducer** と **dispatch** かと思いますので詳しく説明します。

### リデューサ関数の例

以下は、カウンターを管理するシンプルなリデューサです。

```ts
// 状態の型を定義
type State = { count: number };

// アクションの型を定義
type Action = { type: "increment" } | { type: "decrement" }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error('Unknown action type');
  }
}
```

このリデューサ関数は、increment および decrementという2つのアクションタイプを扱います。もちろんこれらのアクションは自分で設定することができます。

`useReducer` を使用する際、リデューサ関数は一般的に switch 文で記述されます。switch 文の各 case において、次の状態（state）を計算して返します。

### dispatch の例

dispatch 関数は、アクションをリデューサへ送るために使用されます。具体的には、ユーザーの操作やイベントハンドラ内で dispatch を呼び出し、状態を更新します。

例を見てみましょう。

```tsx
function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}
```

上記のコンポーネントでは、ボタンのクリックイベントに応じて dispatch 関数を呼び出しています。

:::note info
dispatch 関数の引数は、ユーザによって実行されたアクションです。任意の型の値を指定できます。慣例として、アクションは通常オブジェクトであり、type プロパティで識別され、他のプロパティでオプションの追加情報を保持します。
:::

## 2\. `useState` と `useReducer`の比較と使い分け方

それぞれの特徴を表にしてまとめてみました。

|             | `useState`                           | `useReducer`                    |
|-------------|--------------------------------------|---------------------------------|
| **コード**    | 短い。初期コードは少ない       | 長い。リデューサ関数と dispatch が必要             |
| **可読性**   | シンプルな state 更新に最適      | 複雑な state 更新に最適。ロジックの分離が可能   |
| **デバッグ**  | バグの原因特定が難しい場合がある  | コンソールログで更新の原因（state, action）を追跡可能             |
| **テスト**    | コンポーネント全体でテスト        | 純関数として単体テストが可能                    |
| **好み**     | `useReducer`で代用可能      | `useState`で代用可能

React 公式は、「**バグが頻繁に発生しておりコンポーネントのコードに構造を導入したい場合に、リデューサを利用することをお勧めします**」とのことです。

しかし機能は同等なので、個人の好みやチームの規則に従って選択するのが良さそうです。
同じコンポーネントで `useState` と `useReducer` を両方使うことも可能です。

## 3\. `useState` から `useReducer` への段階的な移行の手順

`useState` から `useReducer` への移行は、次の 3 つのステップで行うことができます。

#### 1\. イベントハンドラからアクションをディスパッチする

リデューサを使った状態管理は state を直接セットするのとは少し異なります。

**React に対して state をセットして「何をするか」を指示するのではなく、イベントハンドラから「アクション」をディスパッチすることで「ユーザが何をしたか」を指定します。**

例えば、以下のような `useState` を使ってタスクを直接的に追加/変更/削除するイベントハンドラがあったとしたら、

```tsx
const [tasks, setTasks] = useState(initialTasks);

// 新しいタスクを追加する
function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}

// 既存のタスクを変更する
function handleChangeTask(task) {
  setTasks(
    tasks.map((t) => {
      if (t.id === task.id) {
        return task;
      } else {
        return t;
      }
    })
  );
}

// タスクを削除する
function handleDeleteTask(taskId) {
  setTasks(tasks.filter((t) => t.id !== taskId));
}
```

このように変更します。

```tsx
function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task,
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId,
  });
}
```

イベントハンドラ内で state を直接更新するのではなく、dispatch にアクションタイプと state の更新に必要な情報を与えてリデューサを呼び出してあげるというイメージです。

#### 2\. state とアクションから次の state を返すリデューサ関数を書く

`useReducer` においては、リデューサ関数が state のロジックを記述する場所です。
現在の state とアクションオブジェクトの 2 つを引数に取り、次の state を返します。

先ほどの tasks の変更のロジックは以下のようになります。

```tsx
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}
```

リデューサ関数は state (tasks) を引数として取るため、コンポーネントの外部で宣言することができます。これにより、インデントレベルが減り、コードが読みやすくなります。

#### 3\. `useState` を `useReducer` に置き換える

最後に、リデューサをコンポーネントに接続する必要があります。React から `useReducer` フックをインポートし、呼び出してください。

以下の `useState` を使った呼び出しを、

```tsx
const [tasks, setTasks] = useState(initialTasks);
```

このように置き換えます。

```tsx
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
```

フックの第一引数に先ほど作成したリデューサをセットしてやれば完成です！
必要であればリデューサを別ファイルに移動することもできます。

`useReducer` を使い、関心を分離することで、コンポーネントのロジックが読みやすくなります。イベントハンドラはアクションをディスパッチすることで「何が起こったか」を指定し、リデューサ関数がそれらに対する「state の更新方法」を決定します。

## おわりに

いかがだったでしょうか。

個人的には `useReducer` は、`useState` を多用した時のコンポーネント内のコードやロジックの煩雑さを解消してくれるという点で、すごく便利だなと感じました。

それにしても React hooks はとても奥が深いですね。またキャッチアップがあれば記事にします。

ここまでお読みいただきありがとうございました！

## 参考

- React 公式リファレンス

https://ja.react.dev/reference/react/useReducer

https://ja.react.dev/learn/extracting-state-logic-into-a-reducer

- mosya React（ハンズオンもあってオススメ👇）

https://mosya.dev/react/dashboard