React

Reactの状態管理で悩む全ての人へ【2022年】

React
この記事は約25分で読めます。

こんにちは。株式会社インプルの平澤です。
Reactの状態管理(グローバルステイト)で迷われたこと、迷っていることはありませんでしょうか?
そんな方々へReactの状態管理の有名ライブラリをご紹介致します。

はじめに

最初に言いますが、どの方法が良いかの正解はありません。「個人的な相性」・「開発チームの相性」・「パフォーマンス」など、どこに重点を置くかでも選択は変わってきます。
それを踏まえた上でご自身のベストプラクティスを見つけてみましょう。

本記事では
1. Zustand
2. XState
3. Jotai
4. Recoil
5. Redux
6. React Hooks(useContext + useReducer)

のサンプルコードと使用感をまとめました。

選定理由

JavaScript Risingstars 2021 において状態管理ランキングで選出された上位5つ + ReactHooksを比較することで今のトレンドを網羅できると考えたためです。(5位のPiniaはvueの状態管理ライブラリになるので除外しています)

JavaScript ライジングスター 2021
2021年のJavaScript白書。フロントエンド、Node.jsフレームワーク、React/Vueエコシステム、ビルドツール、状態管理などのトレンドを徹底調査。

ただnpm trendを確認するとダウンロード数ではやはりReduxが不動の1位です。
ライジングスターでは2021年1年間のGiuhubのスター増加数なので、間違えなく後発ライブラリの方が有利です。
それを踏まえると今webで公開されているReactのソースコードはReduxが多いはずなので、これからReduxのシェア率は下がることがあったとしても業務等で既存のソースコードに触れる可能性がある場合はReduxを理解するのは大事だと思います。
(私はJotaiかRecoil推しです)

jotai vs recoil vs redux vs xstate vs zustand | npm trends
Comparing trends for jotai 2.4.3 which has 638,103 weekly downloads and unknown number of GitHub stars vs. recoil 0.7.7 which has 493,550 weekly downloads and u...

サンプルプロジェクト作成

まずReactのプロジェクトを作成していきます
(※ Nodeの環境構築ができている前提です)

> yarn create vite
     ✔ Project name: … sample
     ✔ Select a framework: › React
     ✔ Select a variant: › TypeScript

Reactのプロジェクトを作成しました。
普段 create-react-app でプロジェクトを作成するとビルドシステムはwebpackですが、上記のコマンドはviteで作成することになります。
viteが絶対に良いとは一概には言えませんが、webpackよりすごく快適に開発を進められると思いますので是非試してみてください。

ではローカルで起動していきましょう。

> cd sample
> yarn
> yarn dev

これでローカルでvite + reactのプロジェクトサーバーが起動しました。

実際に各ライブラリを全て記載したサンプルプロジェクトは下記のGithubからclone、参照できますので必要に応じて使用してください。
サンプルプロジェクトにはtailwind cssをインストールしています。
詳しい導入方法は公式ドキュメントを参照ください。

GitHub - Hizuraky/react-state-management-sample: Reactの状態管理ライブラリ比較のサンプル
Reactの状態管理ライブラリ比較のサンプル. Contribute to Hizuraky/react-state-management-sample development by creating an account on GitHub.

また、上記のプロジェクトはvercelにデプロイしてありますのでローカルで立ち上げなくても動いているものが見れるようにしています

Vite + React + TS

ライブラリ

各ライブラリの
・特徴
・サンプルコード
・平澤’s review (バンドルサイズ / 学習コスト / 使用感 / 総合点)
をまとめました。

サンプルコードは記事に埋め込む関係上で1ファイルでまとめていますが、開発する際にはファイル分けするのが慣習的だと思います。
また、Provider関連も省略していますので必要に応じてGithubを参照してください。

Zustand

Zustand
🐻 Bear necessities for state management in React
特徴

・Simple and un-opinionated
・Makes hooks the primary means of consuming state
・Doesn’t wrap your app in context providers
・Can inform components transiently (without causing render)

ーーーーーー翻訳ーーーーーー
・シンプルで偏見がない
・フックが状態を消費する主要な手段になります。
・コンテキストプロバイダーでアプリを包み込まない
・レンダーを発生させることなく、コンポーネントに一時的に情報を与えることができる

上記の通りですが、reduxやrecoil、useContextのようにProviderで囲う必要がありません。
「レンダーを発生させることなく、コンポーネントに一時的に情報を与えることができる」と記述されているサブスクライブが特徴的な機能に感じます。
Reduxに比べればシンプルですが、Jotaiのシンプルさに比べると少し複雑に感じます。

サンプルコード
import { useState } from "react"
import create from "zustand"

type ZustandState = {
  count: number
  incrementCount: () => void
  removeCount: () => void
  updateCount: (newCount: number) => void
}

const useZustand = create<ZustandState>((set) => ({
  count: 0,
  incrementCount: () => set((state) => ({ count: state.count + 1 })),
  removeCount: () => set({ count: 0 }),
  updateCount: (newCount) => set(() => ({ count: newCount }))
}))

export const ZustandComponent = () => {
  const { count, incrementCount, removeCount, updateCount } = useZustand((state) => state)
  const [inputNumber, setNumber] = useState(0)

  return (
    <div>
      <p>{count}</p>
      <button onClick={incrementCount}>increment</button>
      <div>
        <input value={inputNumber} type="number" onChange={(e) => setNumber(Number(e.target.value))} />
        <button onClick={() => updateCount(inputNumber)}>update</button>
      </div>
      <button onClick={removeCount}>remove</button>
    </div>
  )
}
平澤’s review
項目評価所感
バンドルサイズ★★★★★1.1KB 圧倒的軽量で1位。2位のJotaiの1/3以下という結果に。
学習コスト★★★★☆Reduxがわかる方であればなんの問題もなく導入できるレベル
使用感★★★★★サブスクライブ機能や、ミドルウェアによってReduxと似たような使用法があったりと”使いごたえのある”オプションが多い
総合点★★★★★“比較的”使いやすい部類であり、バンドルサイズは超軽量。特に欠点が見つからなく、小~大規模のプロジェクトまで使える印象。

XState

XState Docs
Documentation for XState: State Machines and Statecharts for the Modern Web
特徴

Statecharts are a formalism for modeling stateful, reactive systems. This is useful for declaratively describing the behavior of your application, from the individual components to the overall application logic.

ーーーーーー翻訳ーーーーーー

ステートチャートは、ステートフルでリアクティブなシステムをモデリングするための形式です。これは、個々のコンポーネントからアプリケーション全体のロジックに至るまで、アプリケーションの動作を宣言的に記述するのに便利である。

XStateはステートチャートに基づいた状態遷移を管理するライブラリです。
他のライブラリと比べると”状態遷移”という部分に特化しており一線を画しています。
こちらのXStateビジュアライザにコードをコピペすると状態遷移が視覚化することができます。
この機能はチームで開発する際など、人に説明するときに非常に便利になると思います。

サンプルコード
import { useState } from "react"
import { useMachine } from "@xstate/react"
import { createMachine, assign } from "xstate"

export type XStateState = {
  count: number
}

export const machine = createMachine<XStateState>(
  {
    id: "xstate",
    initial: "count",
    predictableActionArguments: true,
    context: {
      count: 0
    },
    states: {
      count: {
        on: {
          INCREMENT_COUNT: [{ target: "count", actions: "incrementCount" }],
          REMOVE_COUNT: [{ target: "count", actions: "removeCount" }],
          UPDATE_COUNT: [{ target: "count", actions: "updateCount" }]
        }
      }
    }
  },
  {
    actions: {
      incrementCount: assign((_ctx) => ({
        count: _ctx.count + 1
      })),
      removeCount: assign(() => ({
        count: 0
      })),
      updateCount: assign((_, { value }) => {
        return {
          count: value
        }
      })
    }
  }
)

export const XstateComponent = () => {
  const [inputNumber, setNumber] = useState(0)
  const [current, send] = useMachine(machine)
  const { count } = current.context

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => send("INCREMENT_COUNT")}>increment</button>
      <div>
        <input value={inputNumber} type="number" onChange={(e) => setNumber(Number(e.target.value))} />
        <button onClick={() => send({ type: "UPDATE_COUNT", value: inputNumber })}>update</button>
      </div>
      <button onClick={() => send("REMOVE_COUNT")}>remove</button>
    </div>
  )
}
平澤’s review
項目評価所感
バンドルサイズ★★☆☆☆17.9KB 他ライブラリに比べると少し重め。とはいえそこまで気にするレベルでも無し。
学習コスト★★☆☆☆XStateのライブラリ+ステートチャートの両方を学習する必要がある上に、どちらも簡単なものではない。
使用感★★★★★ステートチャートが唯一無二な機能であり、ビジネスロジックの統一しやすさもGood。
総合点★★★☆☆使用感で記載した内容的にチーム開発に向いている印象。ただ学習コストの高さに少し難あり。時間があるチーム開発には一考の価値あり。

Jotai

Jotai, primitive and flexible state management for React
Jotai takes a bottom-up approach to global React state management with an atomic model inspired by Recoil. One can build state by combining atoms and renders ar...
特徴

Jotai has a very minimal API and is TypeScript oriented. It is as simple to use as React’s integrated useState hook, but all state is globally accessible, derived state is easy to implement, and extra re-renders are automatically eliminated.

ーーーーーー翻訳ーーーーーー

Jotaiは非常にミニマルなAPIを持ち、TypeScriptを指向しています。Reactに統合されたuseStateフックと同じくらいシンプルに使えますが、すべてのステートはグローバルにアクセスでき、派生ステートは簡単に実装でき、余分な再レンダリングは自動的に排除されます。

Jotaiはzustandには劣るものの、非常に軽量なライブラリです。
そして何より使い方が直感的で引用にも記載していますがuseStateを理解していればすんなり使えるというレベルでシンプルです。

サンプルコード
import { useAtom, atom } from "jotai"
import { useState } from "react"

const countAtom = atom(0)

export const JotaiComponent = () => {
  const [inputNumber, setNumber] = useState(0)
  const [count, setCount] = useAtom(countAtom)

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>increment</button>
      <div>
        <input value={inputNumber} type="number" onChange={(e) => setNumber(Number(e.target.value))} />
        <button onClick={() => setCount(inputNumber)}>update</button>
      </div>
      <button onClick={() => setCount(0)}>remove</button>
    </div>
  )
}
平澤’s review
項目評価所感
バンドルサイズ★★★★☆3.4KB zustandには劣るが非常に軽量。
学習コスト★★★★★useStateが理解できていればすぐに使用できるレベル。
使用感★★★★☆使いやすい。特段Jotaiにしかない機能とかはないがシンプルさはダントツ。
総合点★★★★★非常に使いやすい印象。Providerレスであったりなど、1分未満で実装できるレベル。特に個人開発ではこだわりがなければ第一候補か。(開発者は日本の方です)

Recoil

Recoil
A state management library for React.
特徴

Recoil lets you create a data-flow graph that flows from atoms (shared state) through selectors (pure functions) and down into your React components. Atoms are units of state that components can subscribe to. Selectors transform this state either synchronously or asynchronously.

ーーーーーー翻訳ーーーーーー

Recoilでは、アトム(共有状態)からセレクタ(純粋関数)を経て、Reactコンポーネントへと流れるデータフロー・グラフを作成することができます。アトムは、コンポーネントがサブスクライブすることができる状態の単位です。セレクタは、この状態を同期または非同期で変換します。

使い方としてはJotaiと同様にuseStateと同じような形で非常にシンプルです。
ただJotaiがRecoilに寄せて作られた後発ライブラリでJotaiの方がよりシンプルです。
開発元がReactと同じFacebook(Meta)なので強いていうならReactの変更に強いことが挙げられます。

サンプルコード
import { useRecoilState,atom} from "recoil"
import { useState } from "react"

export const countAtom = atom({
  key: "countAtom",
  default: 0
})

export const RecoilComponent = () => {
  const [inputNumber, setNumber] = useState(0)
  const [count, setCount] = useRecoilState(countAtom)

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>increment</button>
      <div>
        <input value={inputNumber} type="number" onChange={(e) => setNumber(Number(e.target.value))} />
        <button onClick={() => setCount(inputNumber)}>update</button>
      </div>
      <button onClick={() => setCount(0)}>remove</button>
    </div>
  )
}
平澤’s review
項目評価所感
バンドルサイズ★★☆☆☆23.0KB 他ライブラリに比べると少し重め。とはいえそこまで気にするレベルでも無し。
学習コスト★★★★★Jotaiと同様にuseStateが理解できていればすぐに使用できるレベル。
使用感★★★★☆シンプルで使いやすい。Jotaiと同様。
総合点★★★★☆特段理由がなければJotaiに軍配といった印象。ただ、webに上がっている情報量ではrecoilの方が多いのでそこに不安があればRecoilの方が良い。

Redux

Redux - A predictable state container for JavaScript apps. | Redux
A predictable state container for JavaScript apps.
特徴

It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.

ーーーーーー翻訳ーーーーーー

一貫した動作をし、異なる環境(クライアント、サーバー、ネイティブ)で動作し、テストが容易なアプリケーションの作成を支援します。その上、ライブコード編集とタイムトラベリングデバッガを組み合わせたような、素晴らしい開発体験を提供します。

状態管理のグランマことRedux。
特徴というより歴史的な部分になってしまいますが、他のライブラリが2-5年前に作られたのに対してReduxは11年経っています。
元々はfacebook(Meta)によって作られたFlux(データフローが単一方向のアーキテクチャ)が発表されてその後にReduxが作られました。
※現在ではFluxプロジェクトはメンテナンスモードです。

以上のことからReduxについての情報量や既存のコードに組み込まれている数は圧倒的トップです。

サンプルコード
import { useState } from "react"
import { useSelector, useDispatch } from "react-redux"
import { createSlice, configureStore } from "@reduxjs/toolkit"

const countSlice = createSlice({
  name: "count",
  initialState: {
    count: 0
  },
  reducers: {
    incrementCount: (state) => {
      state.count += 1
    },
    removeCount: (state) => {
      state.count = 0
    },
    updateCount: (state, { payload }) => {
      state.count = payload
    }
  }
})

const { incrementCount, removeCount, updateCount } = countSlice.actions

export const store = configureStore({
  reducer: countSlice.reducer
})

export const ReduxComponent = () => {
  const { count } = useSelector((state) => state) as { count: number }
  const dispatch = useDispatch()
  const [inputNumber, setNumber] = useState(0)

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch(incrementCount())}>increment</button>
      <div>
        <input value={inputNumber} type="number" onChange={(e) => setNumber(Number(e.target.value))} />
        <button onClick={() => dispatch(updateCount(inputNumber))}>update</button>
      </div>
      <button onClick={() => dispatch(removeCount())}>remove</button>
    </div>
  )
}
平澤’s review
項目評価所感
バンドルサイズ★★☆☆☆17.5KB redux-toolkit+react-reduxを合わせたサイズ。他ライブラリに比べると少し重め。とはいえそこまで気にするレベルでも無し。
学習コスト★★☆☆☆Fluxアーキテクチャを理解した上で使用する必要がある。また古い情報も多いことから選別する必要がある。
使用感★★★☆☆後発ライブラリに比べると少し使いずらさを感じる部分がある。
総合点★★★☆☆他のライブラリでいいかな感が否めない。が、記事の冒頭でも記載したがReduxを理解すること自体は重要。

React Hooks(useContext + useReducer)

フック API リファレンス – React
ユーザインターフェース構築のための JavaScript ライブラリ
特徴

useContext
コンテクストオブジェクト(React.createContext からの戻り値)を受け取り、そのコンテクストの現在値を返します。コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある <MyContext.Provider> の value の値によって決定されます。

useReducer
useState の代替品です。(state, action) => newState という型のリデューサ (reducer) を受け取り、現在の state を dispatch メソッドとペアにして返します(もし Redux に馴染みがあれば、これがどう動作するのかはご存じでしょう)。

Reactのプロジェクトを作成したらすぐに使用できるので、バンドルサイズは実質0カロリー
また、ReactHooksなのでReactのバージョンによる互換性などを気にしなくて良い。

サンプルコード
import { useState, useContext } from "react"
import type { Reducer, Dispatch, ReactNode, FC } from "react"
import { createContext, useReducer } from "react"

type ContextState = {
  count: number
}

type Actions =
  | {
      type: "INCREMENT_COUNT"
    }
  | {
      type: "REMOVE_COUNT"
    }
  | {
      type: "UPDATE_COUNT"
      payload: number
    }

const initialState = { count: 0 }

export const CountContext = createContext(
  {} as {
    state: ContextState
    dispatch: Dispatch<Actions>
  }
)

const countReducer: Reducer<ContextState, Actions> = (state, action) => {
  switch (action.type) {
    case "INCREMENT_COUNT":
      return { count: state.count + 1 }
    case "REMOVE_COUNT":
      return initialState
    case "UPDATE_COUNT":
      return { count: action.payload }
    default:
      return initialState
  }
}

export const CountContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const [state, dispatch] = useReducer(countReducer, initialState)
  return <CountContext.Provider value={{ state, dispatch }}>{children}</CountContext.Provider>
}

export const ContextComponent = () => {
  const { state, dispatch } = useContext(CountContext)
  const [inputNumber, setNumber] = useState(0)

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: "INCREMENT_COUNT" })}>increment</button>
      <div>
        <input value={inputNumber} type="number" onChange={(e) => setNumber(Number(e.target.value))} />
        <button onClick={() => dispatch({ type: "UPDATE_COUNT", payload: inputNumber })}>update</button>
      </div>
      <button onClick={() => dispatch({ type: "REMOVE_COUNT" })}>remove</button>
    </div>
  )
}
平澤’s review
項目評価所感
バンドルサイズ★★★★★実質0カロリー
学習コスト★★★☆☆ReactHooksを理解していたり、Reduxを理解していればすんなり実装できるレベル。
使用感★★★☆☆少し記述が多くなることや、1context,1providerが必要になることを考えると少し使いずらさを感じる
総合点★★★☆☆ライブラリを入れたくない事情があったり、ReactHooksマスターでない限りは他ライブラリを導入するのが吉な印象

まとめ

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

Reduxなどデータが1つの巨大なオブジェクトである場合は不要なレンダリングが起きやすい傾向にあるので、少し注意が必要です。
(dispatchすると5-10回くらいレンダリングされるプロジェクトを見たことがあります)
その点JotaiとRecoilはAtom毎にデータを保持するので不要なレンダリングは防ぎやすくなっています。

やはり状態管理は選択肢が多くて悩む部分が多いと思います。
どのように状態管理をするかはケースバイケースですが、自身に合う方法を1つ見つけておくと開発が楽になるかもしれません。