ReactのState管理を比較してみた (useState / useReducer / React Context API / Redux / Recoil)

おはこんばんちは、[@ちーず](https://qiita.com/cheez921)です。
アドベントカレンダー2日目はReactのステート管理についてです。

Reactでは、いろんな方法でステート管理できますね。
React hooksでは、`useState`や`useReducer`、`useContext`など...
さらにstate管理のライブラリも、`Redux`や`MobX`、`recoil`など色々あって何がなんだかわからない...
そう感じている人もいると思います。(※ 自分です)

それらをどのように使うことができるか、どんな時に使うと良いかなどをまとめてみました！！

## :paperclip: React hooksでのステート管理

### 1. useState
一番基本的なState管理ができるhooksです。

```jsx
// const [state変数, stateの更新関数] = useState(初期値)
const [state, setState] = useState(initialState)
```


### 2. useReducer
useState同様、State管理ができるhooksです。

```js
// reducer関数
// const reducer = (1つ前のState, action) => State
// actionは基本、type と payload を持っている
const reducer = (state, action) => {
  switch (action.type) {
    case "ACTION_NAME":
      return { ...state, newState }
    default:
      return state;
  }
}

// const [state変数, reducerの実行関数] = useReducer(reducer関数, 初期値)
const [state, dispatch] = useReducer(reducer, initialState)
```

`useState`に比べて、少々複雑に感じますね。

シンプルなステート管理は`useState`で事足りるのですが、
下記のようなケースは`useReducer`の方が適していると言われています。

* 複数の値を操作する必要がある複雑なロジック
* 前のstateに基づいて次のstateを操作したい時

詳しくみていきましょう！

#### 事例1. 複数の値を操作する`useReducer`
今回は、名前と電話番号を入力するフォームを想定して実装してみます。

```jsx
import { useReducer, Reducer } from "react";

// Stateの型
type State = {
  name: string;
  telNum: string;
};

// Actionの型
type Action = {
  type: "UPDATE_NAME" | "UPDATE_TEL";
  payload: string;
};

// reducer
const reducer: Reducer<State, Action> = (state, action) => {
  switch (action.type) {
    case "UPDATE_NAME":
      return { ...state, name: action.payload };
    case "UPDATE_TEL":
      return { ...state, tel: action.payload };
    default:
      return state;
  }
};

// 初期値の設定
const initialState = {
  name: "",
  telNum: ""
};


const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <div>
        <label>名前: </label>
        <input
          value={state.name}
          // dispatchを使ってreducerにactionを送る
          onChange={(e) => {
            dispatch({
              type: "UPDATE_NAME",
              payload: e.target.value
            });
          }}
        />
      </div>
      ...
    </>
  );
};
```

`useState`で記述する場合、stateを別々で扱う必要がありますが、
`useReducer`を使うことで**1つのstate**にすることができます。

また、`useReducer`の重要な特徴として、**reducerがstateに依存した関数ではない**こと、
つまり**非依存な純粋関数**です。
そのため、テストの記述のしやすい、可読性があがる、などのメリットがあります。

#### 事例2. `useReducer`を使って記述を短くできるケース
`useReducer`は**前のstateに基づいて次のstateが決まる処理**がお得意です。
そのため、`toggle`を実現するには非常に相性が良いです。

```jsx
// useStateの場合
const [isOpen, setIsOpen] = useState(false)
const toggleIsOpen = () => setIsOpen(!isOpen);

// useReducerの場合
const [isOpen, toggleIsOpen] = useReducer((prev) => !prev, false);
```

`useReducerを使うと1行でかけちゃいましたね！！

#### `useReducer`を使ってパフォーマンス改善？！
`useReducer`の`dispatch関数`はメモ化されます。
そのため、`React.memo`を組み合わせて使うことでパフォーマンスを最適化することができます。

▼ 参考

https://qiita.com/uhyo/items/cea1bd157453a85feebf


しかし、`useState `, `useReducer`はローカルステートであり、
Propsしない限り、単一のコンポーネントの中でしかstateを扱うことができません。

そこで登場するのが`React Context`です。

### 3. React Context API
`React Context API`は、propsのバケツリレーをせずとも、
コンポーネントの階層に関係なくstateを子コンポーネントに伝えることができるAPIです。

```jsx
import { createContext, useState, useContext } from 'react';

// 1. Contextの作成
// (Contextはstateを管理する箱、Storeのようなものと考えればok)
export const Context = createContext(undefined);

// 2. Context.Providerコンポーネントの作成 
// Providerは子コンポーネントにstateを配布できるようにする役割
// 渡したいstateやstate更新関数をvalueに渡すことで、Contextに値を流し込むことができる
export const ContextProvider = ({ children }) => {
  const [state, setState] = useState('');

  return (
    <Context.Provider value={[state, setState]}>
      {children}
    </Context.Provider>
  );
};

// 3. Contextに格納された値をhooksで取得する
// Consumerと役割は同様
export const useStateContext = () => {
  return useContext()
}
```

最初は書き方に慣れないかもしれませんが、
このようにとても簡単にグローバルなステート管理が実現できます。

また、`useReducer`と`React Context`を組み合わせて使えば、
後述するReduxに近しいデータストア設計を作成することもできます。

しかし、`React Context`はいくつかのデメリットがあります。

* `React Context`は複数作ることができるため、データフローの整備が難しい
* ちゃんとメモ化しないと、再レンダリング地獄になる可能性あり...
  * Providerの値が更新される度に、useContextを利用しているコンポーネントはすべて再レンダリングされるため

▼ 参考

https://qiita.com/soarflat/items/b154adc768bb2d71af21


## :notebook_with_decorative_cover: ライブラリを利用したステート管理

個人的には基本的に`React Hooks`が提供してくれているState管理で十分かな、と思っていますが、
世の中にはState管理ができる様々なライブラリがあります。

ライブラリを用いいると、データストアを外部に持たせたり、
データフローのグラフ化やロギングなどよりリッチなState管理ができるものが多いです。

その中から特に人気のライブラリを軽くご紹介します！

### 1. Redux
まずはState管理ライブラリの王といっても過言ではない、`Redux`をご紹介します。

`Redux`は`React Context API`と異なりコンポーネントツリーの外部に**1つのデータストア**を持たせて
Stateの管理ができるライブラリです。

様々なメソッドや管理ツール([Redux Toolkit](https://redux-toolkit.js.org/))が提供されており、
より柔軟にState管理ができます。

▼ 公式ドキュメント

https://redux.js.org/

▼ 参考

https://reffect.co.jp/react/react-redux-for-beginner

アプリケーション全体のStateを一つの場所で管理することができるため、データフローを整備しやすいですが、
一方で全体のStateをオブジェクトで管理しており、常に更新し続けるような設計になっています。
そのため、ちょっとしたコードのミスでStateが一気に消失してしまう可能性も...

そこで生まれたのが`Recoil`です。

### 2. Recoil
`Recoil`はコンポーネントツリーの外部に**Atomというデータストアを複数持たせてStateの管理ができる**ライブラリです。
本家Facebookによって公開されたState管理ライブラリであり、今ちょっとアツめです。(※ 主観です)
(正直自分も使ったことがない、詳しくないため、別途調査して追記していきます...！)

▼ 公式ドキュメント

https://recoiljs.org/


▼ 参考

https://ics.media/entry/210224/

https://blog.uhy.ooo/entry/2020-05-16/recoil-first-impression/



## :question: 結局どれを選べばいいの...？
正直本当にケースバイケースだな、という印象です。

記事を通して筆者が考えた使い分けは以下の通りです！ 
(まだ初学者よりであるため、ご意見いただけるとありがたいです！！)

* シンプルなローカルステート(プリミティブ型、一次元)を扱う → `useState`
* 複雑なローカルステート(オブジェクト)を扱う → `useReducer`
* ローカルステート & 前のstateに基づいて次のstateを操作したい時 → `useReducer`
* グローバルなステート管理をしたい → `React Context API`
* 大規模プロジェクトでデータフローを整備しながらグローバルなステート管理をしたい → `ライブラリを用いる`
    * 1つのデータストアで管理したい → `Redux`
    * 複数のデータストアで管理したい → `Recoil`


また、様々な観点から比較してみた記事もたくさんあるので、
いろんな情報を参考にしてみて、サービスにあったState管理を探してみるのがよいのかな？と思いました！

https://blog.uhy.ooo/entry/2021-07-24/react-state-management/


https://zenn.dev/kazuma1989/articles/68c2339e056530

---

以上、ReactのState管理についてでした！！
明日はuseEffectのお話をします :qiitan: