【React】useStateとuseReducer、使うのはどっち

# 概要
この記事はWano Group Advent Calendar2022 14日目の記事です。
https://qiita.com/advent-calendar/2022/wano-group

Wanoで[Video Kicks](https://www.tunecore.co.jp/video-kicks?via=115&gclid=Cj0KCQiAnNacBhDvARIsABnDa69Iao9-3UA-EV_kKfrduesrPQv0iY11LOWSoHOxNWMAhM-GxBE64qcaAskqEALw_wcB)という
ミュージックビデオをiTunesやApple Music、レコチョクMUSICストア、dミュージック、GYAO!、LINE MUSICなどのビデオ配信ストアで配信/販売できるサービスを開発しています。

私は最近Reactを使い始めたので、useStateとuseReducerの違いと、どちらを使うべきかについて考えていきます。


# 環境
Vite,React,Typescriptを使用しています。

# 結論

#### 基本はuseReducerを使った方がよさそう
理由は、stateが想定外の値で更新されることを未然に防ぐ事ができるからです。


ざっくり言うと、両者には以下のような違いがあります。

- useState・・・state更新処理を使用する側が、更新方法や値を決める

- useReducer・・・使用されるstate更新処理側が、更新方法や値を決める

useReducerであれば、state更新方法をあらかじめ決めておく事ができます。
使用する側はstate更新処理で使用する条件のみ渡せば良いため、極端に言うと更新方法は知る必要がありません。

一方で、useStateではstate更新関数の引数に、更新後の値を直接渡します。
従って、予想外の値が入ってきてエラーとなる可能性があります。

以降では、useState、useReducerのサンプルコードを比較してみます。

# useState

まずは、useStateを使用したサンプルコードを以下に示します。
なお比較しやすいように、実装内容は後述のuseReducerのコードに寄せています。
これは`MenuType`という料理の種別を指定することで、今日の献立(state)を更新するコードです。

実装者である`Aさん`は、`updateState`でstateの更新を行う**つもり**で実装しました。
この**つもり**というのが重要となります。
また、`Aさん`は野菜嫌いなので野菜料理を選択した場合はエラーを吐くよう実装しています。

```menuState.tsx
import { useState } from "react";

const MenuType = {
  Chicken: "Chicken",
  Beef: "Beef",
  Fish: "Fish",
  Vegetable: "Vegetable"
} as const;

type MenuType = typeof MenuType[keyof typeof MenuType];

type Menu = {
  menu: string
};

const initialState: Menu = {menu: ""};

// 使用される側
const updateState = ({type: action}: {type: MenuType}): Menu => {
  switch (action) {
    case MenuType.Chicken:
      return {menu: "唐揚げ"};
    case MenuType.Beef:
      return {menu:"ハンバーグ"};
    case MenuType.Fish:
      return {menu: "寿司"};
    case MenuType.Vegetable:
      throw new Error("野菜は嫌いです");
    default:
      throw new Error("不正な値です");
  }
}

export const MenuState = () => {
  const [state, setState] = useState(initialState);

  return (
    <>
      <div>
        今日の献立: {state.menu}
      </div>
      <div>
        {/* 使用する側 */}
        <button onClick={() => setState(updateState({type: MenuType.Vegetable}))}>野菜</button>
        <button onClick={() => setState(updateState({type: MenuType.Chicken}))}>鶏</button>
        <button onClick={() => setState(updateState({type: MenuType.Beef}))}>牛</button>
        <button onClick={() => setState(updateState({type: MenuType.Fish}))}>魚</button>
      </div>
    </>
  );
}

```

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/6eb96a30-fca7-e9c7-6d01-bfa797927aee.gif" />

野菜選択時は・・・
<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/c0240f5b-1627-7e8a-9d66-cd8050a08778.png" />

### 想定通りエラー吐いてますね！


その後、`Bさん`が次のような実装を行いました。`Bさん`は`Aさん`の野菜嫌いを知りません。

```menuState.tsx

// 省略

export const MenuState = () => {
  const [state, setState] = useState(initialState);

  return (
    <>
      <div>
        今日の献立: {state.menu}
      </div>
      <div>
        {/* 使用する側 */}
        <button onClick={() => setState(updateState({type: MenuType.Vegetable}))}>野菜</button>
        <button onClick={() => setState(updateState({type: MenuType.Chicken}))}>鶏</button>
        <button onClick={() => setState(updateState({type: MenuType.Beef}))}>牛</button>
        <button onClick={() => setState(updateState({type: MenuType.Fish}))}>魚</button>
        {/* ここに追加 */}
        <button onClick={() => setState({menu: "ピーマン"})}>ピーマン</button>
        {/* ここに追加 */}
      </div>
    </>
  );
}

```

するとどうなるか・・・

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/b191694a-fa41-74ee-da2c-d17755f6ec9d.png" />

野菜の献立を禁止しているにもかかわらず、ピーマンが献立に選ばれてしまいました。
エラーも吐かず、正常な動作です。

これは、`Aさん`は`updateState`でstateの値を切り替えているつもりでしたが、
実際は`setState`の引数でstateを更新することになるので起こっています。

このようにuseStateでは、実装者にstate更新後の値が委ねられます。
一人や少人数であればよいですが、複数人で実装する際に`Aさん`と`Bさん`のような認識齟齬が発生し、
想定外の値でstateが更新される恐れがあります。


# useReducer

次に、useReducerのサンプルコードを以下に示します。


useStateと違うのは、stateの更新処理を
```ts
() => dispatch({type: MenuType.Vegetable})
```
このように呼び出している点です。


```menuReducer.tsx
import { useReducer } from "react";

const MenuType = {
  Chicken: "Chicken",
  Beef: "Beef",
  Fish: "Fish",
  Vegetable: "Vegetable"
} as const;

type MenuType = typeof MenuType[keyof typeof MenuType];

type Menu = {
  menu: string
};

const initialState: Menu = {menu: ""};

// 使用される側
const reducer = (prev: Menu, {type: action}: {type: MenuType}): Menu => {
  switch (action) {
    case MenuType.Chicken:
      return {menu: "唐揚げ"};
    case MenuType.Beef:
      return {menu:"ハンバーグ"};
    case MenuType.Fish:
      return {menu: "寿司"};
    case MenuType.Vegetable:
      throw new Error("野菜は嫌いです");
    default:
      throw new Error("不正な値です");
  }
}

export const MenuReducer = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        今日の献立: {state.menu}
      </div>
      <div>
        {/* 使用する側 */}
        <button onClick={() => dispatch({type: MenuType.Vegetable})}>野菜</button>
        <button onClick={() => dispatch({type: MenuType.Chicken})}>鶏</button>
        <button onClick={() => dispatch({type: MenuType.Beef})}>牛</button>
        <button onClick={() => dispatch({type: MenuType.Fish})}>魚</button>
        {/* ここでエラー */}
        <button onClick={() => dispatch({type: MenuType.GreenPepper})}>ピーマン</button>
        {/* ここでエラー */}
      </div>
    </>
  );
}

```

さて、`Bさん`が再びピーマンボタンを実装しようとします。
しかし、IDE(画像はVSCode)の入力補完機能により、dispatchの引数に指定できる値が表示されます。

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/f4cbebda-7c23-e443-687f-4fb718d22e19.png" />

以下のように、想定外の値を渡そうとした場合はその時点でエラーを表示してくれるため実装時に気づく事ができます。
<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/88baf495-9ea4-082f-f8cd-f68ef3817e58.png" />

このように、useReducerでは使用される側（ここでは`reducer()`）がstate更新方法を決めています。
これによって、IDEの入力補完機能を通して`Bさん`に対してstate更新方法を教える事ができます。

# まとめ

特に複数人〜大規模開発では、全てのメンバーに正しく利用方法が伝わるような実装が理想的だと考えます。
今回のuseStateのように、一部の実装者しか知らないようなstate更新方法がある場合、それ以外の実装者への説明（野菜は禁止なのでピーマンを実装しないでほしい、など）が必要となってしまいます。

この工数を減らすためにuseReducerは有効な選択肢となると思います。