🛸

Magic Moment 式 Redux による Store 管理

2021/12/15に公開

Agenda

  • はじめに
  • Store で管理するデータ
  • Store 管理による恩恵
    • Entity の重複を回避できる
    • バケツリレーが減る
  • Redux による Store 管理
    • ディレクトリ構成
    • カスタムフックを使用してコードを綺麗にする
  • まとめ
  • 最後に

はじめに

Magic Moment では、React/TypeScript を採用しています。

React を使っていると一番悩むのは、State をどこで管理するか、というところなのではないでしょうか?前回の React 初心者がゼロから作って苦労した State 管理の話 ではページコンポーネントに絞ってご紹介しましたが、今回は Magic Moment の考える Store 管理についてご紹介したいと思います。

Store で管理するデータ

Magic Moment では以下のものを Store 管理しています。

種類 詳細
Entity APIから取得するデータ
アプリケーションの状態 エラー情報やページ間で使用する情報など
操作者の状態 認証情報やユーザーのProfileなど

Store 管理による恩恵

Entity の重複を回避できる

Store を使うことで Entity の管理が一元化でき、データの重複を回避することができます。

重複を回避することで、以下のような編集画面などで情報を編集した際に、表示が変わる箇所と変わらない箇所が出てしまう問題が発生しなくなります。

1.png

データの持ち方は以下のようになっており、コンポーネントによるデータ管理 の場合はコンポーネントがそれぞれデータを持ち、Storeによるデータ管理 の場合はコンポーネントの外でデータを一元管理しています。

3.png

バケツリレーが減る

Store 管理することによって、親コンポーネントから受け渡しが不要になります。

受け渡しがなくなることで、無駄なレンダリングや冗長なコードを減らすことができます。

2.png

Redux による Store 管理

Store の管理方法は、useReduceruseContext などの hooks を使う方法と ReduxRecoil などのライブリを使う方法があります。

Magic Moment では、web上の情報が多い Redux を採用することにしました。

ディレクトリ構成

ディレクトリ構成は re-ducks パターンを採用しています。

re-ducks パターンを採用することで、リソース毎にファイルが纏まりとても管理しやすいです。

src/store
├── ducks
│   └── user
│       ├── action.ts
│       ├── reducer.ts
│       ├── hook.ts
│       └── type.ts
├── action.ts
├── index.ts
├── reducer.ts
├── hook.ts
└── type.ts

カスタムフックを使用してコードを綺麗にする

Magic Moment は、直接 Store を参照せずカスタムフックを経由して Store にアクセスする方法を採用しています。

理由としては3つあります。

  • 更新、取得、算出が一箇所で行えるようになり可読性があがる。
  • useSelector/useDispatch などの Redux の hook を何度も呼び出す必要がなくなる。
  • Redux 以外のライブラリへの切り替えが容易になる。

例)

/ducks/user/action.ts
APIを使用してユーザーを取得し、reducer に dispatch します。


export const listUsers = (): ThunkAction<void, RootState, undefined, RootAction> => async (dispatch) => {
  const usersRes = await usersApi.usersGet();
  const users = usersRes.data.body.data;

  dispatch({
    type: UserActionType.USER_LIST,
    payload: {
      users,
    },
  });
};

/ducks/user/hook.ts
useMemouseCallback を使用して、算出したデータや Action をメモ化します。

import { listUsers } from '~/store/actions';

const useUserStore = () => {
  const userState = useSelector<RootState, RootState['user']>((state) => {
    return state.user;
  });

  const filterByStatus = React.useCallback(
    (status: User['status']) => {
      return userState.users.filter((user) => {
        return user.status === status;
      })
    },
    [userState.users],
  );

  const activeUsers = React.useMemo(() => {
    return filterByStatus(UserStatus.Active);
  }, [filterByStatus])

  const dispatchListUsers = React.useCallback(async () => {
    await dispatch(listUsers());
  }, [dispatch]);

  return {
    userState,
    filterByStatus,
    activeUsers,
    dispatchListUsers,
  };
};

export default useUserStore;

/views/page/Example.tsx
コンポーネントでは、 useUserStore を使用してメモ化されたデータや関数を利用します。

import { useUserStore } from '~/store/hook';

const Example = () => {
  const { dispatchListUsers, activeUsers } = useUserStore();

  React.useEffect(() => {
    (async () => {
      await dispatchListUsers();
    })();
  }, [dispatchListUsers]);
  
  return (
    <ul>
      {
        activeUsers.map((user) => {
          return <li key={ user.id }>{ user.name }</li>;
        })
      }
    </ul>
  );
};

export default React.memo(Example)

まとめ

今回は Store 管理について紹介しました。

コンポーネントはUI、Store はデータ管理のように責務を分離することで、開発で生じる迷いや予期せぬバグが減ったように思います。

Store 管理に悩まれている方の参考になれば幸いです。

最後に

弊社 Magic Moment ではフロントのアーキテクチャを一緒に考えて開発してくださる方を募集しています!

8/30にはFindy様主催のイベントにMagic Momentから石田さんが登壇されます!
よろしければぜひご視聴ください!

さらに、こちらのイベントも8/29開催予定です!こちらはオンラインイベントです。Magic Momentの開発がどんなものか興味を持っていただいた方は是非ご参加ください!

Discussion