プロダクト開発部の渡邊です。
私が所属するチームでは、React を使ったマイクロサービスの開発を行っています。 Global state については、Zustand というライブラリを使って管理しています。 私は数か月前に他のチームから異動してきたのですが、それまで Zustand を使ったことがなかったので 理解を深めるためにその他の手法との比較をしてみました。
本記事では React の Global state の管理手法について、3通りの手法を比較しながら紹介します。
※サンプルコードを載せていますが、可読性を考慮して一部のコードを省略しています。
React の状態管理の基本と重要性
React の状態管理は、アプリケーションの動的なデータ(状態)を追跡し、ユーザーの操作に応じて UI を更新するために不可欠です。
React では、各コンポーネントが独自の Local state を保持でき、それによりレンダリングされる UI を制御します。 一方で、アプリケーション全体を通じて共有されるデータは(Global state)として管理されます。
本記事では、この Global state の管理方法に焦点を当てます。Global state の管理は、異なるコンポーネント間でのデータの一貫性を保ち、 複雑なデータフローや状態依存のロジックを簡素化することに役立ちます。 適切な状態管理を行うことで、アプリケーションはより効率的に動作し、開発者はメンテナンスやスケーリングを簡単に行えるようになります。
Context API
React の Context API は、React のコアライブラリに組み込まれている状態管理のための API です。 Context API は React のコアライブラリに組み込まれているため、追加のライブラリをインストールする必要はありません。
https://ja.react.dev/learn/passing-data-deeply-with-context
通常、親コンポーネントから子コンポーネントへのデータの受け渡しは、props
を使って行われます。
しかし、コンポーネントツリーが深くなるにつれて、props
を使ったデータの受け渡しは煩雑になります。
// 親コンポーネント function App() { const userProfile = { name: 'コドモン 太郎', age: 5, }; return ( <div> <Children name={userProfile.name} age={userProfile.age} /> </div> ); } // 子コンポーネント function Children(props) { return ( <div> <GrandChildren name={props.name} age={props.age} /> </div> ); } // 孫コンポーネント function GrandChildren(props) { return ( <div> <p>名前: {props.name}</p> <p>年齢: {props.age}</p> </div> ); }
Context API を使うことで、コンポーネントツリー全体でデータを共有できます。 以下の流れで Global state を管理できます。
React.createContext
でContext
オブジェクトを作成するContext.Provider
のvalue
に対してstate
を渡すstate
の提供先のコンポーネントをContext.Provider
で囲う- 提供先のコンポーネントで
React.useContext
を使用してstate
を呼び出す
// 1. React.createContext で Context オブジェクトを作成する const UserProfileContext = React.createContext(null); function App() { const userProfile = { name: 'コドモン 太郎', age: 5, }; return ( // 2. Context.Provider の value に対して state を渡す // 3. state の提供先のコンポーネントを Context.Provider で囲う <UserProfileContext.Provider value={userProfile}> <UserProfile /> </UserProfileContext.Provider> ); } function UserProfile() { // 4. 提供先のコンポーネントで React.useContext を使用して state を呼び出す const userProfile = useContext(UserProfileContext); return ( <div> <h1>ユーザープロフィール</h1> <p>名前: {userProfile.name}</p> <p>年齢: {userProfile.age}</p> </div> ); }
Context API を使うことで、中間コンポーネントがいくつ存在しようとも、下層のコンポーネントでstate
を扱えるようになりました。
注意点としては、Context
の値が変更されると、Provider
でラップされたコンポーネントツリーが再レンダリングされることです。
この動作は頻繁な状態更新がある場合、パフォーマンスの問題につながるため注意が必要です。
今回紹介する手法の中では Context API はもっともシンプルな手法なので、 小規模なアプリケーションや状態の更新が頻繁でないアプリケーションに適しています。
Redux
Redux は、JavaScript アプリケーションのための状態管理ライブラリです。 特に React と組み合わせて使用されることが多く、アプリケーションの状態を単一のストアで管理します。 Redux の主な利点は、状態の一貫性、デバッグのしやすさ、および大規模アプリケーションでのスケーラビリティです。
Redux を React と組み合わせて使用する場合、react-redux
というライブラリを使用します。
Redux では以下の流れで Global state を管理できます。
ActionCreator
がAction
を作成するStore
に対してAction
をDispatch(送信)
するDispatch
されたAction
をReducer
が受け取るReducer
が新しいState
をStore
が保存するStore
から新しいState
を取得してView
に描画する
// actions.js export const UPDATE_USER_NAME = 'UPDATE_USER_NAME'; // 1. ActionCreator が Action を作成する export const updateUserName = newName => ({ type: UPDATE_USER_NAME, payload: newName });
// UserProfile.js (Reactコンポーネント) function UserProfile() { const userProfile = useSelector(state => state.userProfile); const dispatch = useDispatch(); const handleNameChange = () => { // 2. Store に対して Action を Dispatch(送信)する dispatch(updateUserProfile({ name: '田中二郎' })); }; return ( <div> <h1>ユーザープロフィール</h1> {/* 5. Store から新しい State を取得して View に描画する */} <p>名前: {userProfile.name}</p> <p>メール: {userProfile.email}</p> <button onClick={handleNameChange}>名前を変更</button> </div> ); }
// reducers/userProfileReducer.js const initialState = { name: 'コドモン 太郎', age: 5 }; // 3. Dispatch された Action を Reducer が受け取る export default function userProfileReducer(state = initialState, action) { switch (action.type) { case UPDATE_USER_NAME: // 4. Reducer が新しい State を Store が保存する return { ...state, name: action.payload }; default: return state; } }
Redux を使用することで複雑な Global state を管理できるようになります。 コードが冗長に感じることもありますが、ルールが厳密であるため大規模なアプリケーションで開発者の人数が増えてもコードの一貫性を保ちやすくなります。 その一方で学習コストが高く、小規模なアプリケーションではオーバースペックになる可能性があります。
Zustand
Zustand は、JavaScript アプリケーションで使用できる軽量で柔軟な状態管理ライブラリです。 このライブラリの主な特徴は、簡潔なAPIと設定の少なさです。Redux のような厳格なルールに従わずに柔軟に状態管理ができます。
https://github.com/pmndrs/zustand
Zustand を使用する場合、zustand
というライブラリを使用します。
以下の流れで Global state を管理できます。
create
関数でStore
を作成するStore
からstate
とstate
を更新するための関数を取得するstate
をView
に描画するstate
を更新するための関数をView
のイベントに紐付ける
// store.js import create from 'zustand'; // 1. create 関数で Store を作成する const useUserProfileStore = create(set => ({ userProfile: { name: 'コドモン 太郎', age: 5 }, updateName: name => set(state => ({ userProfile: { ...state.userProfile, name } })), updateAge: age => set(state => ({ userProfile: { ...state.userProfile, age } })) })); export default useUserProfileStore;
// UserProfile.js import useUserProfileStore from './store'; function UserProfile() { // 2. Store から state と state を更新するための関数を取得する const { userProfile, updateName, updateAge } = useUserProfileStore(); return ( <div> <h1>ユーザープロフィール</h1> {/* 3. state を View に描画する */} <p>名前: {userProfile.name}</p> <p>メール: {userProfile.age}</p> {/* 4. state を更新するための関数を View のイベントに紐付ける */} <button onClick={() => updateName('コドモン 二郎')}>名前を変更</button> <button onClick={() => updateAge('3')}>年齢を変更</button> </div> ); }
Zustand を使用することで、Redux のような厳格なルールに従わずに柔軟に状態管理ができます。 また、Redux と比較してコードが簡潔になるため、小規模から中規模のアプリケーションに適しています。 その一方で、Redux と比較してルールが緩いため、大規模なアプリケーションではコードの一貫性を保ちにくくなります。
まとめ
本記事では、React の Global state の管理手法について、3通りの手法を比較しながら紹介しました。
私が所属するチームは人数も少なく、複雑な Global state を管理する必要もないため、Zustand が適していると考えています。
ただし、プロジェクトの規模や状態管理の複雑さによっては、Redux や Context API が適している場合もあります。 また、ここで紹介した手法以外にも、MobX や Recoil などの状態管理ライブラリが存在します。
プロジェクトによって適切な手法は異なるため、状態管理の手法を選定する際はプロジェクトの規模や状態管理の複雑さ、 メンバーの状況を考慮して選定することをおすすめします。
最後までお読みいただきありがとうございました!