はじめに

会社のプロジェクトでReact+Reduxで開発を行ってきたのですが、周辺のライブラリやディレクトリ構成など設計部分で悩むところが多くかなり苦戦しました。最近になってだいぶまとまってきたので、共有できればなと思い投稿します。もっといい方法あるよとかあったらコメントいただけると嬉しいです。

Reducer

最初はライブラリなどを使わずに書いていたのですが、ネストが深い場合などにとても辛くなってきたのでimmutable.jsを利用しました。ただしimmutableはstateがプレーンなオブジェクトで返ってこないためわかりづらく、現在はimmer(https://github.com/mweststrate/immer) を使用しています。
(複雑なビジネスロジックが必要な場合などはimmutable.jsでモデルを書く方が良い気がします。)
immerは現在のstateを渡すと下書き状態であるdraftが渡されそれに対して変更を行うと、変更が反映された新しいオブジェウトを返却してくれます。変更は通常のJSオブジェクトと同様に行うことができるので、非常にとっつきやすいと思います。

immer
const initialState = {
  text: ''
}
const sampleReducer = produce((draft, action) => {
  switch (action.type) {
    case CHANGE_TEXT:
      const { text } = action.payload;
      draft.text = text; //draftに対して変更を行うことでstateに変更を行った新しいオブジェクトが返される
      return;
  }
}, initialState);

Action

Actionは必ずtype, payload, metaの三つのプロパティを持つように定義しています。(Flux Standard Actionを参考に)
payloadにはフォームやAPIからのresponseなどstateの更新に必要なデータ、metaにはそれ以外のメタ情報を載せるようにしています。

Store

Storeに関してはnormalizr(https://github.com/paularmstrong/normalizr) を使用し、APIからフェッチしてきたデータをentityとresultに分離して利用しています。entityは一箇所に保存、resultはそれぞれの画面ごとに持つ、という形にすることでentity一箇所に対して変更を行うだけで、全ての表示箇所に更新を反映させることができます。またデータ構造をフラットにすることができるので、ネストされたstateにアクセスする必要がなく更新や削除なども楽に行うことができます。

ネストしたオブジェクト
{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
}
normlizeしたオブジェクト
{
  result: "123", //ここにentityへの参照が入る
  entities: {
    "articles": { 
      "123": { 
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: [ "324" ]
      }
    },
    "users": {
      "1": { "id": "1", "name": "Paul" },
      "2": { "id": "2", "name": "Nicole" }
    },
    "comments": {
      "324": { id: "324", "commenter": "2" }
    }
  }
}

非同期処理

非同期処理はredux-saga(https://github.com/redux-saga/redux-saga) を使用してmiddlewareに記述しています。redux-sagaは非同期処理をタスクとして扱えるようにしてくれるライブラリで、あるアクションがあった際にそのアクションに応じて非同期処理を実行、結果に応じて新しいアクションを発行したりということが可能となります。APIとの通信は、通信開始のアクションを待って通信を行い、成功した時と失敗した時で別のアクションを発行するという形で行なっています。
例えばデータをロードする際にはLOADというアクションを発行、非同期処理を行い成功したらLOAD_SUCCESS、失敗したらLOAD_FAILというアクションを発行するといった感じです。ローディングはLOADの時にtrue, LOAD_SUCCESSの時にfalseにするといった感じです。

ルーティング

ルーティングにはreact-router(https://github.com/ReactTraining/react-router) +react-router-redux(https://github.com/reactjs/react-router-redux) を使っています。APIで投げるクエリはなるだけstateでは持たずにurlに持たせるようにし、componentWillReceivePropsでクエリの変更があった場合に新しくリクエストを投げるという形にしています。クエリをstateとurl双方で持ってしまうとかなり見通しが悪くなるので統一した方が良いかなと思っています。

型定義

型定義にはflow(https://github.com/facebook/flow) を利用しています。flowは静的な型チェックのライブラリで実行前にエラーのチェックを行うことができます。

ディレクトリ構成

.
└── src
    ├── index.js
    ├── api
    ├── containers
    ├── components
    ├── modules
    ├── sagas
    ├── schemas
    ├── types
    └── utils

api

APIの通信用のクラスを定義。Fetch APIを利用してプロミスを返すような関数を作成しています。{error, response}のようなオブジェクトを戻り値として返し、エラーハンドリングを行うようにしています。

containers

Reduxのstoreとconnectしてstateを受け取るコンポーネントを配置します。基本的にルーティングのパス一つ(一画面)につき1コンテナー作成するようにしていますが、独立性の高い部分ではコンテナーのネストも行なっています。

components

ReduxのStoreとconnectしないコンポーネントはこちらに配置しています。アクションなどは直接実行することはできないので、containerからコールバックで渡してもらって、それを実行する形になります。

modules

Action, Action Creator, Reducerをひとまとめにして書いたファイルを配置しています。これらは一つの流れで実行されるので、一つのファイルにおいておいた方が見通しが良くなると思っています。(インポートも一度にできるので楽)
Ducksという書き方を参考にしています。(https://github.com/erikras/ducks-modular-redux)

sagas

redux-sagaのタスクを書いたコードを配置しています。非同期処理やビジネスロジックはほぼsagaに記述しています。

schemas

normalizrで使用するschema(entityの定義)を置いています。

types

flowの型定義ファイルを置いています。

utils

全体で使用するutility系の関数をまとめています。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.