useReducerの本質：良いパフォーマンスのためのロジックとコンポーネント設計

React Hooksの正式リリース（2019年2月）からそろそろ一年が経とうとしています。Hooksの登場によってReactのコンポーネントは関数コンポーネントが一気に主流になり、クラスコンポーネントが新規に作られる機会は激減しました。

また、React 17.x系ではConcurrent Modeの導入とともに[さらに2種類の新フック](https://qiita.com/uhyo/items/6be96c278c71b0ddb39b)が追加される見込みであり、いよいよ関数コンポーネントの能力がクラスコンポーネントを真に上回る時代が来ることになります。

この記事では、フックの一種である**useReducer**に焦点を当てて、どのようなときに`useReducer`が適しているのかを説明します。究極的には、useReducerによって達成できるパフォーマンス改善があり、ときにはそれがコンポーネント設計にまで影響を与えることを指摘します。

useStateの影に隠れたり、なぜかReduxと比較されたりといまいちぱっとしないuseReducerですが、この記事でその真の魅力を知っていただければ幸いです。


## まとめ

- `useReducer`は、ステートに依存するロジックをステートに非依存な関数オブジェクト（`dispatch`）で表現することができる点が本質である。
- このことは`React.memo`によるパフォーマンス改善につながる。
- `useReducer`を活かすには、ステートを一つにまとめることで、ロジックをなるべくreducerに詰め込む。

## 背景: `useReducer`とは

まずは、初心者の方向けに`useReducer`の動作を説明します。すでに知っているという方は次の節まで飛ばしても構いません。

`useReducer`はフックの一種であり、**関数コンポーネントのステートを宣言する**能力を持ちます。ステートの宣言は`useState`と`useReducer`の2種類の方法がありますが、`useReducer`は複雑なロジックが絡んだステートを宣言するのに適しています。

`useReducer`は以下のように使います。こちらが用意するのは`reducer`と`initialState`の2つです。`reducer`は「現在のステート」と「アクション」を受け取って「新しいステート」を返す関数であり、`initialState`はステートの初期値です。

```ts
const [currentState, dispatch] = useReducer(reducer, initialState);
```

`useReducer`の返り値は2つで、`currentState`はステートの現在の値、`dispatch`はアクションを発火する関数です。`dispatch`にアクションを渡すと、内部で`reducer`が呼び出されて新しいステートが計算され、コンポーネントが再レンダリングされて新しいステートが反映されます。

一応簡単な例を示しておきます。まずは`reducer`の例です。分かりやすさのためにTypeScriptを用いています。

```ts
type State = {
  count: number
};

type Action = {
  type: "increment" | "decrement";
};

const reducer = (state: State, action: Action): State => {
  if (action.type === "increment") {
    return {
      count: state.count + 1
    };
  } else {
    return {
      count: state.count - 1
    }
  }
};
```

ここではアクションは`{ type: "increment" }`または`{ type: "decrement" }`です。見て分かる通り、これはそれぞれ「カウンタを1増やす」操作と「カウンタを1減らす」操作に相当します。このreducerによって管理される`State`は`{ count: number }`です。つまり、カウンタの数値をひとつ持っているだけのオブジェクトです。この場合`type State = number`でも別に構いませんが、今後の拡張性を考えてこの定義にしています。

これは`const [state, dispatch] = useReducer(reducer, { count: 0 })`のように使用します。この`dispatch`を用いて、`dispatch({ type: "increment" })`とすればステートが変化してカウンタの値が1増えるでしょう。

これが`useReducer`の使い方です。`useReducer`は、ステートの種類が増えたりロジックが増えたりしてもその操作の窓口が`dispatch`という一点に集約されている点がポイントです。子コンポーネントが何かしらのロジックを発火したいときは`dispatch`をpropsで渡すだけでいいし、コンポーネントツリーが大きい場合はコンテキストを用いて子に伝えるのも有効でしょう。

## `useReducer`がパフォーマンス改善につながる例

Reactアプリのパフォーマンス改善において大きな効果が出やすいのは`React.memo`の活用です（クラスコンポーネント時代の`shouldComponentUpdate`や`PureComponent`に相当）。これを活用して**コンポーネントの余計な再レンダリングを避ける**ことが、Reactアプリの基本的なパフォーマンス・チューニングです。

この例では、`useReducer`が`React.memo`の利用の助けになる例を示し、丁寧に解説します。

### 初期状態のサンプル

まず、改善前の初期状態を見てみましょう。以下のCodeSandboxで実際に動作を確かめることができます。初期状態のコードは`App1.tsx`に入っています。

- https://codesandbox.io/s/qiita-usereducer-7s2k5

今回の題材はこの画像のようなものです。

![2fdbb139875c810db99b763f8a9f7c33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/225d8c49-d4eb-87a5-606b-7c507fc0d607.png)

4つの入力欄があり、それぞれに数値を入力することができます。下には4つの数値を合計した値が表示されます。また、入力欄の横にある「check」ボタンを押すと、そのときの数値が合計の何%かを一番下に表示します。画像は「123」の横のボタンを押したあとの状態です。

一見意味不明な例に見えますが、これは実は筆者が実際に業務で経験した例をかなり単純化したものになっています。

この記事にも初期状態のコードを一気に貼り付けます。記事を読みつつコードを見たいという方は適宜CodeSandboxをご活用ください。記事中でも部分ごとに解説していきますから、ここで全部読む必要はありません。

```tsx:src/App1.tsx
import React, { useState } from "react";
import { sum } from "./util";
import "./styles.css";

const NumberInput: React.FC<{
  value: string;
  onChange: (value: string) => void;
  onCheck: () => void;
}> = ({ value, onChange, onCheck }) => {
  return (
    <p>
      <input
        type="number"
        value={value}
        onChange={e => onChange(e.currentTarget.value)}
      />
      <button onClick={onCheck}>check</button>
    </p>
  );
};

export default function App1() {
  const [values, setValues] = useState(["0", "0", "0", "0"]);
  const [message, setMessage] = useState("");
  return (
    <div className="App">
      {values.map((value, i) => {
        return (
          <NumberInput
            key={i}
            value={value}
            onChange={v =>
              setValues(current => {
                const result = [...current];
                result[i] = v;
                return result;
              })
            }
            onCheck={() => {
              const total = sum(values);
              const ratio = Number(value) / total;
              setMessage(
                `${value}は${total}の${(ratio * 100).toFixed(1)}%です`
              );
            }}
          />
        );
      })}
      <p>合計は{sum(values)}</p>
      <p>{message}</p>
    </div>
  );
}
```

### コードの解説

上記のサンプルのコードを少しずつ解説します。

まず、ひとつの入力欄とボタンのセットが、以下に抜粋する`NumberInput`コンポーネントで表現されています。入力状態は親の`App1`コンポーネントが持つ`values`ステートに保存されており、`NumberInput`自体はステートを持っていません。現在の値は`value`としてpropsを通じて渡されています。これは、「合計を表示する」といったロジックが親コンポーネントにあることから来る必然的な選択です。

```tsx:src/App1.tsx（抜粋）
const NumberInput: React.FC<{
  value: string;
  onChange: (value: string) => void;
  onCheck: () => void;
}> = ({ value, onChange, onCheck }) => {
  return (
    <p>
      <input
        type="number"
        value={value}
        onChange={e => onChange(e.currentTarget.value)}
      />
      <button onClick={onCheck}>check</button>
    </p>
  );
};
```

親コンポーネントである`App`は2つの状態を持ちます。以下に示す`values`と`message`です。

```tsx:src/App1.tsx（抜粋）
  const [values, setValues] = useState(["0", "0", "0", "0"]);
  const [message, setMessage] = useState("");
```

`values`は4つの入力欄の内容が配列で入っています。`message`は「check」ボタンを押したときに表示されるメッセージを管理するステートです。数値の入力が想定されていますが、ステートを数値にしてしまうとちょっと扱いづらいフォームになるので生の入力状態は文字列で持っています。あるあるですね。

ステートの更新部分は`NumberInput`のpropsに渡す関数にベタ書きです。`onChange`が呼び出されたら、`setValues`を呼び出して`i`番目の値が`v`に書き換えた新しい配列を用意してステートを更新します。`onCheck`も同様に、メッセージを組み立てて`setMessage`を呼び出します。

```tsx:src/App1.tsx（抜粋）
onChange={v =>
  setValues(current => {
    const result = [...current];
    result[i] = v;
    return result;
  })
}
onCheck={() => {
  const total = sum(values);
  const ratio = Number(value) / total;
  setMessage(
    `${value}は${total}の${(ratio * 100).toFixed(1)}%です`
  );
}}
```

以上のコードの問題点は、レンダリングのパフォーマンス最適化が何も考えられていないことです。ひとつの数値が変更されるたびに全ての`NumberInput`に再レンダリングが発生してしまいます。

今回のゴールは、**`NumberInput`に`React.memo`を適用して無駄な再レンダリングを減らす**ことです。特に、ひとつの数値が変更されたらその`NumberInput`だけが再レンダリングされて、他の`NumberInput`は再レンダリングされないという状態が理想です。

お察しの通り、最終的には`useReducer`を用いてこれを達成することになります。

### `React.memo`導入への努力

とりあえず、まずは`useState`のまま努力してみましょう。`NumberInput`に`React.memo`を適用して効果を得るためには、他の入力値が変わってもpropsの内容が変化しないようにしなければいけません。現状では`value`は問題ありませんが、`onChange`と`onCheck`が問題です。あの位置に関数をベタ書きということは、これらのpropsには毎回異なる関数オブジェクトが作られて渡されています。これでは`React.memo`は効きません。

こういうときの定石は`useCallback`です。とはいえ、今回はループで`NumberInput`を表示しているのでひと工夫必要です。筋のいい方法としては、`NumberInput`に「自分が何番目か」を表すpropsを渡すという方法があります[^note_usememo]。これをコールバックに渡してもらうことで、`onChange`と`onCheck`は全ての`NumberInput`からのコールバックをひとつの関数で対応できます。

[^note_usememo]: ややアクロバットな別解として、`useMemo`を用いて各`NumberInput`用のコールバックを用意するというものもあります。

以上の工夫を導入して得られたのが、上記のCodeSandboxでいう`App2.tsx`です。全体像を見たいからはCodeSandboxをご覧ください。

- https://codesandbox.io/s/qiita-usereducer-7s2k5

ここでは部分ごとに変更点を見ていきます。まず`NumberInput`です。

```tsx:src/App2.tsx（抜粋）
const NumberInput: React.FC<{
  value: string;
  index: number;
  onChange: (index: number, value: string) => void;
  onCheck: (index: number) => void;
}> = memo(({ value, index, onChange, onCheck }) => {
  return (
    <p>
      <input
        type="number"
        value={value}
        onChange={e => onChange(index, e.currentTarget.value)}
      />
      <button onClick={() => onCheck(index)}>check</button>
    </p>
  );
});
```

`NumberInput`はpropsとして`index`を受け取るようになりました。これが、自身が何番目かを表す数値です。`onChange`と`onCheck`の型も変更され、これらの関数には`index`がオウム返しで渡されるようになっています。先ほども説明した通り、これにより`onChange`と`onCheck`を各`NumberInput`ごとに異なる関数を用意する必要が無くなります。

次に、`App`の変更点を見ます。まずレンダリング部分だけ抜粋すると、こうなりました。

```tsx:src/App2.tsx（抜粋）
  return (
    <div className="App">
      {values.map((value, i) => {
        return (
          <NumberInput
            key={i}
            index={i}
            value={value}
            onChange={onChange}
            onCheck={onCheck}
          />
        );
      })}
      <p>合計は{sum(values)}</p>
      <p>{message}</p>
    </div>
  );
```

`NumberInput`に渡すpropsに`index`が追加されているのに加え、`onChange`と`onCheck`が事前に用意されるようになりました。次に、これらを用意する部分のコードです。

```tsx:src/App2.tsx（抜粋）
export default function App() {
  const [values, setValues] = useState(["0", "0", "0", "0"]);
  const [message, setMessage] = useState("");

  const onChange = useCallback((index: number, value: string) => {
    setValues(values => {
      const newValues = [...values];
      newValues[index] = value;
      return newValues;
    });
  }, []);
  const onCheck = useCallback(
    (index: number) => {
      const total = sum(values);
      const ratio = Number(values[index]) / total;
      setMessage(
        `${values[index]}は${total}の${(ratio * 100).toFixed(1)}%です`
      );
    },
    [values]
  );

  return /* 省略 */
}
```

`onChange`と`onCheck`は`useCallback`に囲まれています。それぞれの関数の中身は、`index`を引数で受け取るようになった以外は変わりません。

できることは全部やったように見えますが、**残念ながらこのコードはまだ目的を達成できていません**。`onChange`は`useCallback`により常に同じ関数オブジェクトになっているのでOKですが、`onCheck`が問題です。

`onCheck`は`useCallback`の第二引数が`[values]`となっています。これは、`values`が変わるたびに、すなわち何か入力が変わるたびに、`onCheck`が作りなおされるということを意味します。これにより`NumberInput`に渡される`onCheck`関数が毎回別物になるため、`React.memo`が無意味になっています。

では、なぜ`useCallback`の第二引数が`values`を含んでいなければいけないのでしょうか。それはもちろん、**`onCheck`が`values`に依存している**からです。つまり、`onCheck`が中で「入力値の合計」を求めるために`values`を使用しているのです。`onCheck`のインターフェースが`(index: number) => void`である、すなわち`index`のみを受け取るという関数である以上、`values`というデータについては`onCheck`に内包されていなければいけません。これにより、必然的に`values`が変わるたびに`onCheck`という関数は別物になります。

一方で、`onChange`は`values`に依存していません。これは、`useState`が提供するステート更新関数が、関数によるステートの更新をサポートしているからです。上のコードでは`setValues`関数の引数として「現在の状態を受け取って次の状態を返す関数」を渡しています。この機能により、`onChange`から`values`への依存を消しているのです。

となると、**`onCheck`が`message`というステートを更新するにあたって、それとは別の`values`というステートに依存していることが問題**だと分かります。これを解消するためには、**2つのステートを合体させて1つのステートにする**必要があります。

このような状況に適しているのが`useReducer`です。ということで、`App`を`useReducer`を用いて書き換えることで問題を解決しましょう。（一応、`useState`を使っていても2つのステートをまとめて問題を解決することはできますが、その状況でわざわざ`useReducer`ではなく`useState`を使う意味は薄いのでここでは考えません。）

### `useReducer`による解決

ということで、最終版です。全体像は以下のCodeSandboxの`App3.tsx`でご覧ください。

- https://codesandbox.io/s/qiita-usereducer-7s2k5

まず、`useReducer`を使うのでreducerを用意しましょう。今回何気なくTypeScriptを使っているので型定義もちゃんとあります。

```tsx:src/App3.tsx（抜粋）
type State = {
  values: string[];
  message: string;
};

type Action =
  | {
      type: "input";
      index: number;
      value: string;
    }
  | {
      type: "check";
      index: number;
    };

const reducer = (state: State, action: Action) => {
  switch (action.type) {
    case "input": {
      const newValues = [...state.values];
      newValues[action.index] = action.value;
      return {
        ...state,
        values: newValues
      };
    }
    case "check": {
      const total = sum(state.values);
      const ratio = Number(state.values[action.index]) / total;
      return {
        ...state,
        message: `${state.values[action.index]}は${total}の${(
          ratio * 100
        ).toFixed(1)}%です`
      };
    }
  }
};
```

型定義を読むと、`State`は`values`と`message`をひとつにまとめたオブジェクトであることが分かります。アクションは`"input"`と`"check"`の2種類があり、それぞれ前回のコードの`onChange`と`onCheck`に相当するロジックが書かれています。

次に`NumberInput`のコードです。

```tsx:src/App3.tsx（抜粋）
const NumberInput: React.FC<{
  value: string;
  index: number;
  dispatch: Dispatch<Action>;
}> = memo(({ value, index, dispatch }) => {
  return (
    <p>
      <input
        type="number"
        value={value}
        onChange={e =>
          dispatch({
            type: "input",
            index,
            value: e.currentTarget.value
          })
        }
      />
      <button
        onClick={() =>
          dispatch({
            type: "check",
            index
          })
        }
      >
        check
      </button>
    </p>
  );
});
```

propsとして受け取るのは`value`, `index`, `dispatch`になりました。従来の`onChange`と`onCheck`がひとつにまとまっていますね。それ以外は特に変わっていません。

最後に`App`コンポーネントのコードです。ロジックが`reducer`の中に移ったのでこちらはシンプルになりました。

```tsx:src/App3.tsx（抜粋）
export default function App() {
  const [{ values, message }, dispatch] = useReducer(reducer, {
    values: ["0", "0", "0", "0"],
    message: ""
  });

  return (
    <div className="App">
      {values.map((value, i) => {
        return (
          <NumberInput key={i} index={i} value={value} dispatch={dispatch} />
        );
      })}
      <p>合計は{sum(values)}</p>
      <p>{message}</p>
    </div>
  );
}
```

ステートの宣言は`useReducer`により行われています。従来`onChange`と`onCheck`が担っていたロジックは`reducer`の中に押し込められましたので、ここでは何もせずにただ`NumberInput`に`dispatch`を渡すだけになっています。

前のコードと比べると、ここに本質的なポイントがあります。それは2つのステートがひとつの`useReducer`に押し込められたことにより、「`values`を見て`message`を決める」という計算が「今のステートから次のステートを計算する」という枠組み（reducer）の中に入ったことです。よって、それを呼び出す側である`dispatch`は**ステートに非依存の関数**となりました。`NumberInput`のpropsは`index`, `value`, `dispatch`だけとなり、自分以外の値が変わっても再レンダリングされることは無くなりました。これで目標達成です。

## ポイントの整理

改めてポイントを整理すると、今回の最も重要だったことは「**ステートの更新関数をステートに非依存にする**」ということでした。`useReducer`の場合は、更新関数（`dispatch`）が非依存であることが保証されています。従来のコード（2番目の例）では`onCheck`という関数がステート（`values`）に依存している関数だったのでうまくいきませんでした。

ステートの更新関数をステートに非依存にするには、「**現在のステートを受け取って次のステートを計算する**」ということを徹底する必要がありました。`useState`の場合は**ステート更新関数に関数を渡す**のを徹底することになります。つまり`setValues(newValues)`ではなく`setValues(currentValues => {...; return newValues })`とするということです。従来のコードでは`onChange`ではこれができていましたが、`onCheck`ではできていませんでした。

これを改善するために今回行なったことは「**2つのステート（`values`と`message`）を1つに合体させる**」ということです。これにより、`onCheck`でも関数によるステート更新ができるようになりました。実を言えば`useState`でも頑張ればこれは達成できますが、このような複雑なステートを扱うには`useReducer`が適しているのでここでは`useReducer`を選択することになります。`useReducer`を使う場合はステート更新関数（`dispatch`）は自動的にステートに非依存になります（`reducer`はそもそも「現在のステートを受け取って次のステートを計算する」というものであるため）。

## `useReducer`のすすめ

このように、`useReducer`を用いることで、**ステート更新関数をステート非依存にする**ことを強制できます。実際のアプリ開発においては、アプリが複雑化するにつれて、あるステートと別のステートが関わりを持ち始めるかもしれません。もっと具体的に言えば、**あるステートを更新するときに別のステートを見る必要が発生する**かもしれません。そのときが**`useReducer`導入のサイン**です。ぜひリファクタリングして`useReducer`を導入しましょう。

なぜ`useReducer`が必要なのか、この記事を読んだ皆さんはしっかりと説明できることでしょう。**ステート更新関数がステートに非依存であることは、`React.memo`の活用には必須**だからです。

また、`useReducer`と`React.memo`の恩恵を最大限受けるためには、**できるだけ`reducer`にロジックを詰め込む**ことが鍵となります。そのためには、**アプリの状態は何でもステートで表現する**ことが重要です。言い換えれば、これは**手続き的なロジックを書かず、状態は明示的・宣言的に扱う**ということです。

また、`useReducer`を活かすためにはそのための**コンポーネント設計**も重要です。今回の例では多少天下り的でしたが、**`NumberInput`が`index`をpropsで受け取るようにした**という点にこれが表れています。`dispatch`を呼び出して自分の`value`を更新するためには自分が何番目かを`dispatch`に教える必要があるからです（`"input"`アクションが`index`を含んでいたことを思い出しましょう）。

## 副作用はどうするのか？　あとReduxの話

ところで、今回の例では「check」ボタンを押すと起こることが「別のステートが更新される」でした。なので、`useReducer`によってステートをひとつにまとめることで、`onCheck`コールバックをステートに非依存にすることができたのでした。

では、もし「check」ボタンを押すと起こることが何らかの**副作用**だったらどうするのでしょうか。例えば、押すとHTTPリクエストが発生するとかです。現時点では、**副作用はreducerの中に書くべきではない**という原則がありますから、この記事で使った手を使うことはできません。

残念なことに、**現時点では対処法はありません**。副作用をどこかのコールバック関数に書いた時点で、その関数がステートに依存することとなり、`React.memo`によるパフォーマンス改善の妨げになります。



実は、これに対する一つの解が**Reduxの使用**です。Reduxを用いたステート管理の場合、**Reduxミドルウェア**の活用によって、ステートに依存する副作用ですら`dispatch`の中に押し込めてステート非依存性を達成できてしまうのです。**Reduxの本質はReactのツリーの外でステートを管理してくれること**であり、それにより**React本体のみでは困難なステート非依存性が実現**しているのです。Reduxはただステート管理に関する統一的な方法論を与えるだけでなく、このようなパフォーマンス上のメリットもあるということは覚えておいて損はないでしょう。

### React 17.x 系の展望

しかし、React 17.x系（いわゆる**Concurrent Mode**が導入されると期待されています）ではまた情勢が変わると筆者は期待しています。端的に言えば、Concurrent Modeにおいては（主に非同期的な）**副作用ですらステート内で管理される**ようになるでしょう。そのための道具がSuspenseです。詳細はそのうち別の記事でお届けしようと思いますが、Concurrent Modeでは副作用とステート管理の概念が大きく様変わりし、Reduxなどに頼らずともパフォーマンス的に最適な副作用の扱いが達成できる場面が増えると予期されます。

## `useRef`に関する注意

ところで、「コールバック関数がステートに依存するのが問題」ということであれば、`useRef`で解決できると思った方も多いでしょう。実際、以下のようにすれば`onCheck`を`values`に非依存にすることができます。

```tsx:src/App4_useRef.tsx（抜粋）
  const [values, setValues] = useState(["0", "0", "0", "0"]);
  const [message, setMessage] = useState("");

  const valuesRef = useRef<string[]>([]);
  valuesRef.current = values;

  const onCheck = useCallback((index: number) => {
    const values = valuesRef.current;
    const total = sum(values);
    const ratio = Number(values[index]) / total;
    setMessage(`${values[index]}は${total}の${(ratio * 100).toFixed(1)}%です`);
  }, []);
```

この例では`values`の値はつねに`valuesRef.current`に反映され、`onCheck`は`values`を参照するかわりに`valuesRef.current`を参照するようにしています。`useRef`が返す`valuesRef`は常に同じオブジェクトであることが保証されていますから、`onCheck`は`valuesRef`に依存することはありません。この方法でも`React.memo`を活用するという目的は達成できています。

しかし、筆者はこの方法はお勧めしません。なぜなら、このように`useRef`を使うのは**React 17.x系でうまく動作しなくなる可能性がある**からです。Concurrent Modeにおいては、refへの書き込みはもはや副作用と見なされます。関数コンポーネントの処理中にこのようにrefへの書き込みを行うのは思わぬ動作を引き起こす可能性があるのです（特にレンダリングが中断される場合）。

このことは実は[Reactの公式ドキュメント](https://ja.reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback)にも明記されています。「将来的にはより使いやすい代替手段を提供することを計画しています」とありますので、React 17.x系ではよりよい別の手段が提供されるかもしれません。

## まとめ

この記事では、**コールバック関数がステートに依存する場合に、`React.memo`の恩恵を受けられない**という問題に対して`useReducer`を用いて対処する方法を示しました。ポイントは**ステート更新関数をステート非依存にする**ことであり、（`useState`でもそれは可能なものの）`useReducer`はそのような書き方に適しています。

記事冒頭のまとめを再掲しておきます。

- `useReducer`は、ステートに依存するロジックをステートに非依存な関数オブジェクト（`dispatch`）で表現することができる点が本質である。
- このことは`React.memo`によるパフォーマンス改善につながる。
- `useReducer`を活かすには、ステートを一つにまとめることで、ロジックをなるべくreducerに詰め込む。

`useReducer`はreducerを用いてステート更新を記述できるものでしたが、reducerの存在価値は単にReduxと同じ書き方ができるというだけではありません。useReducerはこの記事で説明したような本質的な問題を解決するための優れた道具なのです。

`useState`に比べると使い方がややこしいので尻込みしてしまうかもしれませんが、`useState`を多く並べれば並べるほど、いざ必要になったときのリファクタリングが難しくなります。時期を見極めて`useReducer`を導入しましょう。