113
74

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-02

おはこんばんちは、@ちーずです。
アドベントカレンダー2日目はReactのステート管理についてです。

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

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

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

1. useState

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

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

2. useReducer

useState同様、State管理ができるhooksです。

// 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

今回は、名前と電話番号を入力するフォームを想定して実装してみます。

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を実現するには非常に相性が良いです。

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

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

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

useReducerを使ってパフォーマンス改善?!

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

▼ 参考

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

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

3. React Context API

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

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()
}

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

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

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

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

▼ 参考

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

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

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

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

1. Redux

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

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

様々なメソッドや管理ツール(Redux Toolkit)が提供されており、
より柔軟にState管理ができます。

▼ 公式ドキュメント

▼ 参考

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

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

2. Recoil

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

▼ 公式ドキュメント

▼ 参考

:question: 結局どれを選べばいいの...?

正直本当にケースバイケースだな、という印象です。

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

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

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


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

113
74
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
113
74

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?