Reactの一大テーマ、「状態管理」
私は今までReduxを使っていました。ただ、最近ライブラリがどんどん増えてきた中で色々試したくなったので、この機会に見てみます。
今回は、代表的な状態管理ライブラリ「Redux」、「Zustand」、「Jotai」で簡単なカウンターアプリを例にそれぞれメリット・デメリットを比較しました。
興味ある部分だけ見て自分に合うライブラリを見つける参考にしてもらえれば嬉しいです。
ちなみに...
「Recoil」も有名ライブラリの一つなのですが、2023年で更新が止まりメンテナンスされていないみたいなので今回候補からは外しました
そもそも状態管理って何?なんで必要?
(初心者向け超ざっくり)
「状態管理」はわかると言う方は次のセクション「今のトレンドを見ておく」まで飛んでください
「状態管理」の「状態 (state)」とは、簡単に言うと「コンポーネントが持つデータ」のことです。
例えば、+のボタンを押すとカウンターの数字が1ずつ増えるという簡単なカウンターアプリを例に考えます。
画面を描画した状態(=つまり初期値)では、カウンターの数字は0である必要があります(0からカウントを始めるから)。この「カウンターの数字」をReactでは「状態 (state)」とします。そして、+が1回押されるごとに「カウンターの数字」に1を足すという処理をして状態(state)を更新(カウンターの数字の状態を0から1に、1から2に...)します。この「状態の更新」は、状態に変化があるたびに繰り返されます。
import { useState } from "react"
const App = () => {
// countが状態(state), setCountはcountを更新する時に使う
const [count, setCount] = useState(0)
return(
<>
{/* カウンターの数字(count)を表示 */}
<p>{count}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}></button>
</>
)
}
export default App
この「カウンターの数字」はそのコンポーネント内(↑だとApp.tsx内)だけで使う場合はそのまま使えるのですが、例えば子コンポーネントなど別のコンポーネント内でも「カウンターの数字」を使いたい場合はpropsとしてデータを渡してあげる必要があります。
import { useState } from "react"
// 子コンポーネント
const Child = ({ count }: { count: number }) => {
return <p>{count}</p>
}
const App = () => {
const [count, setCount] = useState(0)
return(
<>
<button onClick={() => setCount((prevCount) => prevCount + 1)}></button>
<Child count={count} />
</>
)
}
export default App
このとき例えばひ孫コンポーネントで利用したい場合は、このようになります。
import { useState } from "react"
// ひ孫コンポーネント
const GreatGrandChild = ({ count }: { count: number }) => {
// ここで表示
return <p>{count}</p>
}
// 孫コンポーネント
const GrandChild = ({ count }: { count: number }) => {
return <GreatGrandChild count={count} />
}
// 子コンポーネント
const Child = ({ count }: { count: number }) => {
return <GrandChild count={count} />
}
const App = () => {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount((prevCount) => prevCount + 1)}></button>
<Child count={count} />
</>
)
}
export default App
このコードを見ると、カウンターの数字の状態(count)を定義したのはApp.tsxで、そのcountを表示したいのはひ孫コンポーネントです。つまり、子コンポーネントと孫コンポーネントでは一切countを利用しないのにも関わらず、ひ孫コンポーネントにcountを渡すためだけにpropsで受け取ったものをそのままpropsとして子コンポーネントに渡しています。
これがいわゆるバケツリレーと呼ばれる問題で、propsでデータをやり取りする場合には原則子コンポーネントにしか渡せないため、孫コンポーネントやひ孫コンポーネントでそのデータを利用したい場合にはデータを利用しないコンポーネントを経由しないと渡すことができず、状態の定義元がわかりにくかったり不要なコード量が増えることでコードの可読性が下がってしまいます。
今回は状態(state)がcountだけだったためそこまで大きくコード量に変化はないですが、これが何十個もの状態を管理しなくてはいけないものを同様の方法で実装するとかなり煩雑で保守性の低いコードになってしまいます。
ここで登場するのがReduxやZustandをはじめとした状態管理ライブラリです!
状態管理ライブラリ(または useContext)を使うことで、あるコンポーネント内で定義した状態(state)を他のどのコンポーネントからも簡単に呼び出すことができます。
今のトレンドを見ておく
State of React 2023(Stack Overflowが毎年出すDeveloper Surveyみたいなトレンドランキング)によると、
最も使われていた、もしくは使われているのはuseState(小規模だと状態管理ライブラリ自体必要ないケースが少なくないので当たり前っちゃ当たり前ですね)、
2, 3位にRedux(Redux Toolkit)、
4位Zustand、
5位MobX、
6位Jotaiということで、
Reactの状態管理ライブラリのトレンド的には、やはりReduxまだまだ強いなといった感じですが、意外とZustandも上位だなと思いました。
ちなみにランキングではそのライブラリ(機能)について肯定的か否定的かについてのバロメータもあるのですが、Reduxは使ったことがある人のうち約30%は何かしらのネガティブな感想をもっているみたいです。
気になる方は↓のリンクから他のランキングもチェックしてみると面白いと思います。
https://2023.stateofreact.com/en-US/libraries/state-management/
状態管理ライブラリの紹介
ここからは状態管理についてライブラリを使わない方法、また各ライブラリについて取り上げていきます。
1. ライブラリを使わないシンプルさ (useState / useContext)
useStateとuseContextの特徴
useStateはReactの基本的なフックで、コンポーネント内のローカル状態を管理します。useContextは、コンポーネントツリーのどこからでも状態にアクセスできるようにするためのフックです。この2つを組み合わせることで、簡単なグローバル状態管理が可能です。
公式サイト
https://ja.react.dev/reference/react/useState
https://ja.react.dev/reference/react/useContext
メリット
- React標準のフックのみで追加のライブラリがいらないシンプルさ
- ライブラリと比べると直感的で学習コストが低い
- 比較的小規模なプロジェクトに向いている
デメリット
- 状態が複雑になったり、コンポーネントツリーが深くなると管理が難しくなる
= スケーラビリティ(拡張性)が低い - 状態の一元管理には向いていないため、大規模なアプリには不向き
useStateとuseContextでのカウンターアプリの実装
import UseContextCounter from "./components/UseStateContextCounter"
import { createContext, useState } from "react"
// contextの型定義
interface CounterContextProps {
count: number
increment: () => void
decrement: () => void
}
// contextの作成
export const CounterContext = createContext<CounterContextProps | undefined>(
undefined
)
const App = () => {
// stateの定義
const [count, setCount] = useState<number>(0)
// +ボタンを押した時の処理
const increment = () => setCount((prevCount) => prevCount + 1)
// −ボタンを押した時の処理
const decrement = () => {
if (count === 0) return
setCount((prevCount) => prevCount - 1)
}
return (
// contextの提供
<CounterContext.Provider value={{ count, increment, decrement }}>
<UseContextCounter />
</CounterContext.Provider>
)
}
export default App
import { useContext } from "react"
import { CounterContext } from "../App"
const UseContextCounter = () => {
// contextの値を取得
const { count, increment, decrement } = useContext(CounterContext) ?? {}
return (
<div>
<p>{count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
)
}
export default UseContextCounter
2. 状態管理ライブラリの王道 (Redux)
Reduxの特徴
Reduxは、アプリケーション全体の状態を一元管理して、複数のコンポーネント間での状態共有や更新を効率的に行えるようにしたライブラリです。時間をかけて積み重ねられたエコシステムとコミュニティのサポートが豊富です。Reactにおける状態管理ライブラリといえばReduxというくらい、王道のライブラリです。
ちなみに...
Reduxは今回も含めてReactとセットで紹介されることがかなり多いため、React専用のライブラリだと思われがちですが、Redux単体でも使うことができます
※初めて学ぶ人はRedux単体で学んだ方が概念など理解しやすい人もいるかもです
公式サイト
https://redux.js.org/
メリット
- 状態の一元管理ができ、アプリケーション全体の状態を把握しやすくなる
- アクションとリデューサーに基づく厳密な状態管理により、コードの一貫性や保守性が向上する
- すべての状態の変更が「アクション」と「リデューサー」を通じて行われ、この一方向のデータフローのおかげで、アプリの動作が予測しやすくデバッグもしやすい
デメリット
-
何と言っても『学習コストが高い!!!』これに尽きる。。
※大規模プロジェクトで便利に管理しやすい分、新たに学ぶ概念が比較的多くまたそれぞれも抽象的で難しめ -
初期設定やコードの量が比較的多く、小規模なプロジェクトには過剰
※小規模でも使えないことはないけど、基本オーバースペックなことが多い
Reduxでのカウンターアプリの実装
まず必要なライブラリをインストールします。
npm install @reduxjs/toolkit react-redux
次にReduxでカウンターアプリを実装していきます。
export const INCREMENT = "counter/increment"
export const DECREMENT = "counter/decrement"
interface IncrementAction {
type: typeof INCREMENT
}
interface DecrementAction {
type: typeof DECREMENT
}
export type CounterActionTypes = IncrementAction | DecrementAction
export const increment = (): IncrementAction => ({
type: INCREMENT,
})
export const decrement = (): DecrementAction => ({
type: DECREMENT,
})
import { Reducer } from '@reduxjs/toolkit'
import { CounterActionTypes, DECREMENT, INCREMENT } from './actions/action'
// カウンターの型定義
interface CounterState {
count: number
}
// カウンターの初期状態を定義
const initialState: CounterState = {
count: 0,
}
const counterReducer: Reducer<CounterState, CounterActionTypes> = (state: CounterState = initialState, action: CounterActionTypes): CounterState => {
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + 1,
}
case DECREMENT:
if (state.count === 0) return state
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
export default counterReducer
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../counterReducer";
// storeの作成
const store = configureStore({
reducer: {
counter: counterReducer,
}
})
// 型定義
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export default store
import { RootState } from '../store/store'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from '../actions/action'
const ReduxCounter = () => {
// useSelectorでstateの値を取得
const count = useSelector((state: RootState) => state.counter.count)
// useDispatchでactionをdispatch
const dispatch = useDispatch()
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(increment())}>+</button>
</div>
)
}
export default ReduxCounter
import { Provider } from "react-redux"
import ReduxCounter from "./components/ReduxCounter"
import store from "./store/store"
const App = () => {
return (
// Providerでラップしてstoreを渡す
<Provider store={store}>
<ReduxCounter />
</Provider>
)
}
export default App
3. シンプルで無駄を省いたスマートな状態管理 (Zustand)
Zustandの特徴
Zustandは、Reactアプリケーションのためのシンプルで軽量な状態管理ライブラリです。
使いやすく直感的なAPIを提供し、状態管理の複雑さを大幅に軽減。
Reduxほど厳密な設計を求めず、Storeの分割ができるなど柔軟性があります。
ワンポイント豆知識
「Zustand」はドイツ語で「状態」を意味する単語です。
公式サイト
https://zustand-demo.pmnd.rs/
メリット
- シンプルなAPIで直感的に使える
- 厳密なルールに縛られないため、必要な部分だけを選んで使える高い柔軟性
- 必要な部分のみを更新することで、実現する高速レンダリングによる高パフォーマンス
デメリット
- 小規模から中規模のプロジェクトには最適だが、大規模プロジェクトでは管理が複雑になる場合がある
- Reduxに比べるとエコシステムやネットの情報が少ないため、エラー解決など苦労することも...
Zustandでのカウンターアプリの実装
まず必要なライブラリをインストールします。
npm install zustand
次にZustandでカウンターアプリを実装していきます。
// import create from 'zustand'は非推奨のwarningが出るので注意
import { create } from 'zustand'
// storeの型定義
interface CounterState {
count: number,
increment: () => void,
decrement: () => void,
}
// storeの作成
export const useStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({count: state.count + 1})),
decrement: () => set((state) => ({count: state.count > 0 ? state.count - 1 : state.count})),
}))
import { useStore } from '../store'
const ZustandCounter = () => {
// storeの状態と関数を取得
const { count, decrement, increment } = useStore()
return (
<div>
<p>{count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
)
}
export default ZustandCounter
import ZustandCounter from "./components/ZustandCounter"
const App = () => {
return (
<>
<ZustandCounter />
</>
)
}
export default App
4. アトムベースで簡単・直感的な状態管理 (Jotai)
Jotaiの特徴
Jotaiは、アトム(小さな状態単位)という状態管理を提供することで、個々の状態を独立して管理できるように設計されています。これにより、複雑な状態管理をシンプルに行うことが可能です。
Jotaiは、Redux, Zustandなどの一元管理と違い、アトムを使った分散型の状態管理のため、通常、管理は分散しており必要に応じてアトム同士を組み合わせます。
公式サイト
https://jotai.org/
メリット
- 各状態を独立して管理でき、複雑な状態間の依存関係も比較的容易に扱える
- シンプルで小規模なコードベースで、必要な部分のみを操作可能
- 最小限のAPIながら、Recoilの上位互換とも言われる高い柔軟性
デメリット
- まだ比較的新しいため、ドキュメントやコミュニティサポートが他と比べて少ない
- アトムにより状態管理が散らばるため、全体の管理がやや分かりにくくなることがある
Jotaiでのカウンターアプリの実装
まず必要なライブラリをインストールします。
npm install jotai
次にJotaiでカウンターアプリを実装していきます。
import { atom } from "jotai"
// カウンターの値を管理するアトム
export const countAtom = atom<number>(0)
import { useAtom } from 'jotai'
import { countAtom } from '../atoms'
const JotaiCounter = () => {
// countの値と更新関数を取得
const [count, setCount] = useAtom<number>(countAtom)
// カウントの増減をする関数
const increase = () => setCount((prevCount) => prevCount + 1)
const decrease = () => setCount((prevCount) => prevCount > 0 ? prevCount - 1 : prevCount)
return (
<div>
<p>{count}</p>
<button onClick={decrease}>-</button>
<button onClick={increase}>+</button>
</div>
)
}
export default JotaiCounter
import JotaiCounter from "./components/JotaiCounter"
const App = () => {
return (
<>
<JotaiCounter />
</>
)
}
export default App
各ライブラリの比較
useState/useContext | Redux | Zustand | Jotai | |
---|---|---|---|---|
学習コスト | 低い | 高い | 低い | 中程度 |
設定の記述 | 少ない | 多い | 少ない | 少ない |
一元管理 | × | ◯ | △ | × |
柔軟性 | ◎ | ◯ | ◎ | ◎ |
パフォーマンス | ◎ | ◯ | ◎ | ◎ |
エコシステム | △ | ◎ | △ | △ |
コミュニティサポート | ◯ | ◎ | ◯ | △ |
推奨プロジェクト規模 | 小〜中規模 | 大規模 | 小~中規模 | 小~中規模 |
どのライブラリを選ぶべきか?
それぞれのおすすめ対象者はこんな感じです。
useState/useContext
・React初心者で、とりあえず状態管理についてざっくり学びたい、使ってみたい
・状態 (state)の数が比較的少ない小規模プロジェクトで使いたい
Redux
・useState/useContextでは物足りなくなってきた人
・状態管理について時間をかけてじっくりと学びたい
・状態 (state)管理が比較的複雑な中・大規模プロジェクトで使いたい
・状態は一元管理したい
・王道のライブラリの実装を学びたい
Zustand
・Reduxほどの機能と厳格さは求めないけど、useState/useContextでは物足りなくなってきた人
・一元管理したいけどシンプルで軽量なライブラリを使いたい
・状態 (state)の数が比較的少ない小・中規模プロジェクトで使いたい
Jotai
・Reduxほどの機能は求めないけど、useState/useContextでは物足りなくなってきた人
・中規模程度のプロジェクトで使いたい
・後々拡張したりするための柔軟性は欲しい
終わりに
今回ZustandやJotaiなど初めて使ってみて、Reduxに比べてかなり実装が手頃だなと思いました。従来使っているReduxは学習コストが高いため、なかなかチームに導入するとなるとそこが大きなネックになっていましたが、ZustandやJotaiは新規プロジェクトであれば割と簡単に導入でき学習コストもReduxに比べると低いので、今後案件でも導入候補の上位に入ってきそうです。
採用拡大中!
アシストエンジニアリングでは一緒に働くフロントエンド、バックエンドのエンジニア仲間を大募集しています!
少しでも興味ある方は、カジュアル面談からでもぜひお気軽にお話ししましょう!
お問い合わせはこちらから↓
https://official.assisteng.co.jp/contact/
参考
https://ja.react.dev/reference/react/
https://redux.js.org/
https://zustand.docs.pmnd.rs/getting-started/introduction
https://jotai.org/