「React × Reduxのディレクトリ構成のベストが分からない」
「もしかして、私のRedux用ファイル…多すぎ?」
「Ducksパターンを採用したけど、actionとreducerが肥大化しがち」
こんな悩みを抱える方のために、React×Reduxのディレクトリ構成の決定版「re-ducksパターン」をご紹介します。
ということでこんにちは!
株式会社PlaygroundのWebエンジニアの稲垣です。
Reactでアプリ開発をする際に、ほとんどセットで導入するのがRedux。
しかし、Reduxを導入した途端にファイル数が増え、そのディレクトリ構成は複雑になりがちです。
そこで今回オススメするのが「re-ducksパターン」というディレクトリ構成です。
- re-ducksパターンが生まれた理由
- re-ducksパターンを使うメリット
- re-ducksパターンの実装方法と各ファイルの役割
それでは早速解説していきます。
目次
re-ducksパターンが生まれた理由
まず、re-ducksパターンの前身にはDucksパターンがありました。
Ducksパターンは、Reduxに必要なActionType, Reducer, Actionたちを「機能ごと」に1つのファイルにまとめます。
それまでActionやReducerなど役割ごとに散らばっていたファイルを、まとめて管理しやすくしたのです。
src/
// Ducksパターンが生まれる前のディレクトリ構成
actions
├ curriculum.js
├ posts.js
└ users.js
reducers
├ curriculum.js
├ posts.js
└ users.js
src/
// Ducksパターンによるディレクトリ構成
modules
├ curriculum.js
├ posts.js
└ users.js
機能ごとにファイルがまとまっているので、1ファイルを参照・変更すれば良いというシンプルさが強みでした。
しかし、アプリの規模が大きくなるとファイルが肥大化する、というデメリットもありました。
肥大化したファイルは管理しづらいですからね。
そこで登場したのがre-ducksパターンです。
React×Reduxのディレクトリ構成でre-ducksパターンを使う3つのメリット
re-ducksパターンは、Ducksパターンと同様に機能ごとに分割します。
しかし、1機能=1ファイルではなく、1機能=1ディレクトリです。
そして機能ごとのディレクトリ内でいくつかのファイルを用意します。
以下のような構成です。
src/re-ducks
// re-ducksパターンによるディレクトリ構成
curriculum
├ actions.js
├ index.js
├ operations.js
├ reducers.js
├ selectors.js
└ types.js
posts
├ actions.js
├ index.js
├ operations.js
├ reducers.js
├ selectors.js
└ types.js
users
├ actions.js
├ index.js
├ operations.js
├ reducers.js
├ selectors.js
└ types.js
re-ducksパターンを使うメリットは以下です。
- actionとreducerがシンプルになる
- ファイルが肥大化しにくい
- ファイルごとの役割が明確なので管理しやすい
re-ducksパターンのメリット1|actionやreducerがシンプルになる
re-ducksパターンにはoperationという新キャラが登場します。
詳細は後述しますが、redux-thunkを使うと複雑化しがちなactionファイルの役割の一部を、operationが担ってくれるわけです。
そして、actionファイルは純粋なactionのみを記述することになります。
また同様に、reducerも1つのファイルとして切り出してしまいます。
その結果、reducerファイルの役割は、Switch文でactionのtypeに応じて更新するstateをStoreに渡すだけです。
Simple is Best !!!
re-ducksパターンのメリット2|ファイルが肥大化しにくい
Ducksパターンで1ファイルにまとめていたコードを、re-ducksパターンでは複数ファイルに分割するので、当然ファイルが肥大化しにくくなります。
当然です。
re-ducksパターンのメリット3|ファイルごとの役割が明確なので管理しやすい
re-ducksパターンでは、機能ごとにディレクトリを分け、さらに役割ごとにファイルを分けます。
ファイルごとの役割が明確なので、「どのファイルのどの箇所に何を書けばいいのか」すぐに判断できるのがメリットです。
最初はファイル数の多さに戸惑いますが、慣れてしまえばモーマンタイ。
唯一の欠点がこの「ファイル数の多さ」ですが、保守性を高めるためには目を瞑りましょう。
re-ducksパターンで使う各ファイルの役割【ソースコード例付き】
それでは、re-ducksパターンで使うファイルの役割と、ソースコード例をご紹介します。
re-ducksパターンで使うファイルの役割1|actions
actionsファイルには、純粋にRedux Actionのみを記述します。
src/re-ducks/users/actions.js
export const FETCH_USERS = "FETCH_USERS";
export const fetchUsersListAction = (usersList) => {
return {
type: "FETCH_USERS",
payload: usersList
}
};
re-ducksパターンで使うファイルの役割2|reducers
reducersファイルは、発行されたActionを受け取り、Switch構文内でaction.typeに応じてStoreにstateを渡します。
まじでこのswitch文だけのファイルになります。
src/re-ducks/users/reducers.js
import * as Actions from 're-ducks/users/actions';
import {initialState} from 're-ducks/store/initialState';
export const UsersReducer = (state = initialState.users, action) => {
switch (action.type) {
case Actions.FETCH_USERS:
return {
...state,
users: action.payload
};
// 中略
default:
return state
}
};
re-ducksパターンで使うファイルの役割3|operations
さて、新キャラのoperationです。
redux-thunkを使って非同期処理を制御するようなActionのコードは、全てoperationに任せます。
そしてoperationsは最後にActionを発行(dispatch)します
また、コンポーネントからActionを発行する場合は、必ずoperationを経由するようにしましょう。
src/re-ducks/users/operations.js
export const fetchUsersList = () => {
return async (dispatch: any) => {
let usersList: UsersList = [];
return db.collection('users').get()
.then(snapshot => {
for (let i = 0 ; i < snapshot.size; i = (i + 1) | 0) {
const doc = snapshot.docs[i];
const data = doc.data();
usersList.push({
icon: data.icon
uid: doc.id,
username: data.username,
})
}
dispatch(fetchUsersListAction(usersList))
})
}
}
上記は、FirebaseのFirestoreに非同期処理のクエリを投げ、取得したユーザーリストをActionの引数に渡している処理です。
re-ducksパターンで使うファイルの役割4|selectors
またまた新キャラのselectorです。
この子は本当に便利で、Storeに保存されているstateを参照する関数を提供します。
reselectというパッケージをnpm installして、importしてくださいね。
src/re-ducks/users/selectors.js
import { createSelector } from "reselect";
import State from 're-ducks/store/types'
const usersSelector = (state: State) => state.users;
export const getUserId = createSelector(
[usersSelector],
state => state.uid
);
selectorsファイル内で定義した関数をコンポーネントでimportして使います。
HooksのuseSelector()を使ってStore内のルートstateを取得し、selectorで定義した関数の引数に渡してあげましょう。
src/App.js
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)
return(
<div>ユーザー名は{uid}</div>
)
}
export default App
re-ducksパターンで使うファイルの役割5|types
typesファイルは、TypeScriptで記述する場合に使うファイルです。
JavaScriptで書いているなら不要です。
action, operation, reducerなどで使う型を定義しておきます。
src/re-ducks/users/types.ts
export interface UsersInfo {
icon: string
uid: string
username: string
}
export interface UsersList extends Array<UsersInfo>{}
re-ducksパターンで使うファイルの役割6|index
ただのエントリポイントです。
ぶっちゃけ要らなくね?と思って、僕は使っていません。
ということでre-ducksパターンの解説は以上になります!
「とっつきづらそうだな…」と思うかもしれませんが、1度使い始めたら病みつきになります。(どういうこと)
個人的にはselectorがお気に入りです。
特に、Speaker Deckで解説している石井直矢さんは、僕がお世話になった『React入門』の共同著者の1人。スライドがわかりやすかった。
なお、書籍は「入門」と謳っているが中級者向けなので悪しからず。
- 無料・簡単・片手でホームページを作成できる自社サービス Rakwi
- Web制作とアプリ開発を学べるオンラインプログラミング講座 Upstairs
- 開発,DX推進支援サービス スタートアッププラン