コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

React における Global state の管理法を比較してみた

プロダクト開発部の渡邊です。

私が所属するチームでは、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 を管理できます。

  1. React.createContextContextオブジェクトを作成する
  2. Context.Providervalueに対してstateを渡す
  3. stateの提供先のコンポーネントをContext.Providerで囲う
  4. 提供先のコンポーネントで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 の主な利点は、状態の一貫性、デバッグのしやすさ、および大規模アプリケーションでのスケーラビリティです。

https://redux.js.org/

Redux を React と組み合わせて使用する場合、react-reduxというライブラリを使用します。 Redux では以下の流れで Global state を管理できます。

  1. ActionCreatorActionを作成する
  2. Storeに対してActionDispatch(送信)する
  3. DispatchされたActionReducerが受け取る
  4. Reducerが新しいStateStoreが保存する
  5. 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 を管理できます。

  1. create関数でStoreを作成する
  2. Storeからstatestateを更新するための関数を取得する
  3. stateViewに描画する
  4. 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 などの状態管理ライブラリが存在します。

プロジェクトによって適切な手法は異なるため、状態管理の手法を選定する際はプロジェクトの規模や状態管理の複雑さ、 メンバーの状況を考慮して選定することをおすすめします。

最後までお読みいただきありがとうございました!