概要
ReactとReduxを実業務で使用していて、そのフォルダ構成について説明したいと思います。
- APIの呼び出しにはredux-sagaを使用し、
- コンポーネントの分類にはAtomic Designを採用しています。
- 多人数で開発しやすいよう、action/reducer/sagaのファイルは極力分割しています。
ディレクトリ構成
先にディレクトリ構成を記述して、その後詳細を説明していきます。
この記事は、例としてプロジェクトのタスク管理アプリを想定したファイル名やモデル名を記述します。
- action/
- component/
- atoms/
- molecules/
- organism/
- templates/
- pages/
- container/
- reducer/
- saga/
- libs/
- common/
- model/
- service/
- api/
React/Redux に関するディレクトリ
Action/Component/Container/Reducer
Reduxの役割ごとにディレクトリを構成しています。
- action ... acitonとaction creatorを格納します。ドメインごとに小分けで用意し、index.jsで結合します。
export * from "./user.js"
export * from "./project.js"
export * from "./task.js"
export const CREATE_USER_START_EVENT = "CREATE_USER_START_EVENT"
export const CREATE_USER_SUCCESS_EVENT = "CREATE_USER_SUCCESS_EVENT"
export const CREATE_USER_FAILURE_EVENT = "CREATE_USER_FAILURE_EVENT"
// ...
export const createUserStartEvent = (name, password, email) => ({
type: CREATE_USER_START_EVENT,
payload: {
name: name,
password: password,
email: email
},
})
// ...
- components ... Atomic Designの5分類のサブディレクトリを構成します。
- container ... containerを格納します。
- reducer ... reducerを格納します。ファイルはcombineReducers関数を使って小分けに定義します。
import { combineReducers } from "redux"
import userState from "./userState"
import projectState from "./projectState"
import taskState from "./taskState"
const appState = combineReducers({
userState,
projectState,
taskState,
})
export default appState
saga
sagaのハンドラとなるジェネレーター関数とそのforkを行うファイルを格納します。
これも、以下のようにして小分けにします。
saga.jsでイベントハンドラ関数を順次forkしています。
import UserSaga from "./userSaga"
import ProjectSaga from "./projectSaga"
import TaskSaga from "./taskSaga"
export default function* rootSaga() {
let sagaFunctions = []
sagaFunctions = sagaFucntions.concat(UserSaga.sagaFunctions())
sagaFunctions = sagaFunctions.concat(ProjectSaga.sagaFunctions())
sagaFunctions = sagaFunctions.concat(TaskSaga.sagaFunctions())
// forkに登録
for (let i = 0; i < sagaFuncs.length; i++) {
yield fork(sagaFuncs[i])
}}
}
func *handleCreateUserEvent() {
const action = yield take(CREATE_USER_START_EVENT)
const {createdUser, err} = yield call(
UserService.createUserAsync,
action.payload.name,
action.payload.password,
action.payload.email,
}
if(!err) {
yield put(createUserSuccessEvent(createdUser))
} else {
yield put(createUserFailureEvent(err))
}
}
// ...
class UserSaga {
static sagaFunctions = () => {
return [
handleCreateUser,
handleUpdateUser,
handleDeleteUser,
// ...
]
}
Atomic Design
Atomic Designとは、componentの分類手法で、化学に例えて5つに分類します。
詳細は以下を参照してください。
ディレクトリ構成は以下の5つです。
- atoms/ ... 原子=それ以上分離できない最小のコンポーネント(テキストボックス、ボタン)
- molecules/ ... 分子=原子の組み合わせ(検索ボタン付きのテキストボックス)
- organisms/ ... 組織=そのままで画面部品となりえるもの(ロゴと検索バー、検索リストのテーブル)
- templates/ ... テンプレート=ページの元となるもの。組織、分子、原子を置ける。
- pages/ ... ページ=テンプレートに情報を付加したもの。
libs ディレクトリ(自作のライブラリを格納するディレクトリ)
commonには、ID生成やI18n用や、数値フォーマットのメソッド等を入れます。
- libs/
- common/
- idutil.js
- i18n.js
- formater.js
- model/
- user.js
- project.js
- task.js
- service/
- userService.js
- projectService.js
- taskService.js
- converter/
- userConverter.js
- projectConverter.js
- taskConverter.js
- api/
- userApi.js
- projectAndTaskApi.js
Model層とService層とAPI層
以下の3層に分離しまています。
- Reactのコンポーネントで表示しやすいビューのためのモデル
- ロジックとビューモデルを扱うService、APIを呼び出す。
- ServiceのモデルとAPIのリクエストとレスポンスを変換するConverter
- Serviceから呼び出され、バックエンドのAPIを呼ぶAPI層
Model層
ビュー用のモデルを定義します。例えばUser,Project,Taskなどです。
ES2015のクラスで定義しています。
export default class User {
constructor(id, name, password, email) {
this.id = id
this.name = name
this.password = password
this.email = email
}
// ...
}
Service層
ビュー用モデルやそのパラメータを使ってロジックを実行する層です。
非同期メソッドの場合は、createUserAsyncなどのようにAsyncをつけて命名します。
API層を呼び出す前に、Converterでリクエストとレスポンスを変換します。
Service層のConverter
APIとサービスの橋渡しを行います。
ほとんどのメソッドは、パラメータやビューモデルと、リクエストとレスポンスの変換を行うだけになります。
API層
バックエンドのAPIを呼び出すメソッドです。
ServiceとAPIとConverterは以下のようなコードになります。
class UserService {
static createUserAsync = async (name, password, email) => {
const request = UserConverter.convertParamsToCreateUserRequest(name, password, email)
return UserApi.createUser(request)
.then(response => {
const createdUser = UserConverter.convertCreateUserResponseToUser(response)
return ({ createdUser })
})
.catch(error => ({ error }))
}
// ...
}
まとめ
この構成で半年近く実装していますが、特に違和感なく使えています。
特にレイヤー化することで、以下の恩恵を受けています。
- ModelとAPIの密結合を防ぐ
- API変更は、API層とConverterのみにとじられる
- ロジックはServiceに集約されるので、SagaやReducerが肥大化しない
この構成の例が参考になれば幸いです