Help us understand the problem. What is going on with this article?

React+Redux+APIサーバーでのアプリケーションのディレクトリ/ファイル構成

More than 1 year has passed since last update.

概要

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で結合します。
index.js
export * from "./user.js"
export * from "./project.js"
export * from "./task.js"
project.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関数を使って小分けに定義します。
reducer/index.js
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しています。

saga.js
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])
  }}
}
userSaga.js
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のクラスで定義しています。

user.js
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は以下のようなコードになります。

userService.js
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が肥大化しない

この構成の例が参考になれば幸いです

tashxii
tasとhxiiの共有Qiitaです。
tis
創業40年超のSIerです。
https://www.tis.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした