Redux導入時のディレクトリ構成例

Reduxを導入して起こる作るファイルと記述の量が多い問題

Reduxを勉強し始めてまず感じたのは、定型構文が多く、作るファイルも記述も多い事。
これは、実際に開発を進めるとチーム内で、共通のルールが無いとすぐにカオスな事になるなと感じました。調べたところ、その問題を解決するために、公式、非公式に、いくつかのデザインパターンが提唱されてきたようです。この記事では、その中でディレクトリ構成に絞ってまとめました。

re-ducksパターンとは

開発対象の規模にもよるのかと思いますが、個人的には、re-ducksパターンが一番整理しやすいと感じました。ファイル管理をしやすくするためのReduxのディレクトリ構成で、Ducksパターンから派生したものです。

re-ducks

reduxのディレクトリ構成例

redux-way

actionsとreducers、Reduxの役割別にディレクトリを分けて管理するパターン。
action creators, action types,reducersは、密結合にも関わらず、異なるファイルに分割され、異なるディレクトリに分散しているため、関連性が掴みにくい構成になっている。(基本的には、同じ名前のファイルが複数作られる、かつ開発中に同じ名前のついたファイル間の移動が頻繁に起こることが予想されるため、自分が今どの役割のファイルを記述をしてるのか、混乱しやすいかも...)

以下はsrcがルートとなります。

components
├ users.js
└ articles.js

containers
├ users.js
└ articles.js

actions
├ users.js
└ articles.js

reducers
├ users.js
└ articles.js

types
├ users.js
└ articles.js

Ducksパターン

機能ごとに分割し、一つのファイル内に、actions、types、reducersなど必要な記述を全て行う。機能単位で管理するので、上記のディレクトリ構造と異なり、1ファイルを参照、または変更すれば良いというシンプルな構成。
action creators, action types,reducersの定義が1ファイルに記述するので、見通しが良くなる。

しかし、デメリットもあって、1ファイル内に必要な記述を書いてくので、開発が進むにつれて、ファイルが肥大化しやすく、閲覧性が悪くなる。

components
├ users.js
└ articles.js

containers
├ users.js
└ articles.js

modulues
├ users.js
└ articles.js

re-ducksパターン

usersやarticles単位でディレクトリを切って、その中に必要なファイルを分割して定義する。 Ducksパターンの1機能に1ファイルでなく、1機能に1ディレクトリという点が異なる。

密結合なファイル群が同一のディレクトリに束ねられ、ファイルごとの役割が明確になり、管理しやすくなる点がメリットとして挙げられる。

components
├ users.js
└ articles.js

containers
├ users.js
└ articles.js

users
├ actions.js
├ index.js
├ operations.js
├ reducers.js
├ selectors.js
└ type.js

articles
├ actions.js
├ index.js
├ operations.js
├ reducers.js
├ selectors.js
└ type.js

re-ducksパターンに登場する各ファイルの役割

operations

複雑な処理を書くファイルで、redux-thunk等で非同期処理を制御するようなActionは全てこのファイルに書くことになっています。

そして、最後に__Action__をdispatchするようになっています。 コンポーネントからActionを発行する際は、必ずoperationsファイルを経由する決まりがあります。

↓例えば、SignInの処理を書いた場合

import { signInAction } from './actions';
import { push } from 'connected-react-router';

export const signIn = () => {
  return async (dispatch, getState) => {
    const state = getState();
    const isSignedIn = state.users.isSignedIn;

    if(!isSignedIn) {
      const url = 'https://******';
      const response = await fetch(url).then(res => res.json()).catch(()=> null);

      const username = response.login;
      console.log(username);

      dispatch(signInAction({
        isSignedIn: true,
        uid: uid,
        username: username
      }))

      dispatch(push('/'))
    }
  }
}

types

TypeScriptを導入してる場合は、型定義はこのファイルで行います。

export interface ArticleState {
  title: string
  body: string
}

selectors

Storeで管理しているstateを参照する関数を提供します。 reselectというnpmモジュールを使います。

import { createSelector } from "reselect";

const usersSelector = (state) => state.users;

export const getUserId = createSelectore(
  [usersSelector],
  state => state.uid
);

Appコンポーネント

上記のselectorsファイル内で定義した関数をimportして使います。

useSelectorを使用して、Store内のstateを取得し、selectorsからimportした関数の引数に渡してあげます。

import React from "react";
import {useSelector} from "react-redux";
import {getUserId} from "re-ducks/users/selectors"

const App = () => {
    const selector = useSelector(state => state)
    const uid = getUserId(selector)
    const username = getUserName(selector)

    return(
         <div>ユーザーID{uid}</div>
         <div>ユーザー名は{username}</div>
    )
}

export default App

各役割については、こういうものだという事は分かったが、これは実際に何度か書いて、理解を深めないとなかなか難しい印象。。。規模の大きい開発では、re-ducksを徐々に取り入れてくか、初めから意識しておいた方が良さそうな印象です。