Reduxを導入して起こるであろうファイルと記述量が多くなる問題
Reduxを勉強し始めてまず感じたのは、定型構文が多く、作るファイルも記述も多い事。
これは、実際に開発を進めるとチーム内で、共通のルールが無いとすぐにカオスな事になるなと感じました。調べたところ、その問題を解決するために、公式、非公式に、いくつかのデザインパターンが提唱されてきたようです。この記事では、その中でディレクトリ構成に絞ってまとめました。
re-ducksパターンとは
開発対象の規模にもよるのかと思いますが、個人的には、re-ducksパターンが一番整理しやすいと感じました。ファイル管理をしやすくするためのReduxのディレクトリ構成で、Ducksパターンから派生したものです。
reduxのディレクトリ構成例
パターン導入前の構成
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ファイルを参照、または変更すれば良いというシンプルな構成。定義が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, getUserName} 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を徐々に取り入れてくか、初めから意識しておいた方が良さそうな印象です。