この記事はフューチャー Advent Calendar 2019 12日目の記事となります。
概要
ここ2年ほどWEBフロントエンド界隈で仕事をすることが多く、Backbone.js, Vue.js, Angularといったフーレームワークを渡り歩いてきましたが、2019年は React + Redux での開発を初めてリードすることになりました。
本記事では実際に React + Redux で開発を推進するに当たって悩ましかった部分や勘所について整理出来ればと思います。特に各種ライブラリの選定やRedux構成要素の責務や分割単位にフォーカスしてご説明します。
本記事でご紹介する内容はあくまで一事例であり、個人的な見解も多分に含まれることをご了承ください。
様々なご意見をコメントでお待ちしております。
対象読者
- React + Redux の概要レベルの知識がある方
- React + Redux でこれからチーム開発を始める方
- React + Redux の各種ライブラリ選定に悩んでいる方
- React + Redux のコンポーネント構成や分割単位に悩んでいる方
環境情報
- React: 16.7.0
- Redux: 4.0.1
規模・体制
- 画面数: 約30画面
- 開発メンバ: 約6名(React + Redux 初学者)
Reduxの基礎
Reduxは、Reactが扱うUIのstate(状態)を管理をするためのフレームワークです。
Reduxの基礎的な考え方については先人の素晴らしい記事たちがあるので、ここでは割愛します。
Redux入門【ダイジェスト版】10分で理解するReduxの基礎
https://qiita.com/kitagawamac/items/49a1f03445b19cf407b7
言語選定
React + Reduxを始めるにあたってJavaScriptで開発を行っても良いのですが、筆者はTypeScriptが好きです。(いきなりエモい)
特に今回はフロントエンドの開発経験がないメンバが殆どだったので、型システムの恩恵に預かることで品質を高めていきたいという想いがあり、採用に至りました。
React + ReduxとTypeScriptを組み合わせるに当たって、思ったより情報量がないなというのが最初の所感でしたが、結局は公式のドキュメントが参考になります。
- Redux公式
https://redux.js.org/recipes/usage-with-typescript - TypeScript公式
https://github.com/Microsoft/TypeScript-React-Starter
各種ライブラリ選定
React自体がViewに特化したフレームワークということもあり、フルスタックな機能を実現するにあたっては諸々のライブラリを選定・導入する必要があります。
今回はその中でも代表的な機能のライブラリ選定にあたっての所感を記載してみました。
非同期処理
SPA開発においてAPIコールなどの非同期処理は避けて通れません。
ReduxではAPIをリクエストとレスポンスをactionとして取り扱うべきというお作法があります。
これはAPIが「コールされた」, 「成功した」, 「失敗した」, 「完了した」 という状態を通常は管理する必要があるためです。
ライブラリ候補
React + Redux で非同期処理を実現するためのライブラリはいくつかあります。
https://redux.js.org/faq/actions#what-async-middleware-should-i-use-how-do-you-decide-between-thunks-sagas-observables-or-something-else
-
redux-thunk
https://github.com/reduxjs/redux-thunk
Reduxで非同期通信を行うための代表的なミドルウェアです。
関数を返却するactionクリエーターを返却するというのが特徴的な非常にシンプルかつ軽量なライブラリです。 -
redux-saga
https://github.com/redux-saga/redux-saga
redux-thunk
が「actionを関数にする」のに対し、redux-sagaは非同期処理をSaga
と呼ばれる独立したプロセスで行うため、actionをプレーンな状態に保ったまま非同期処理を実現できるのが特徴的です。
redux-thunk
よりも複雑な分、機能が豊富で並列化やスロットルが実現できます。 -
redux-observable
https://github.com/redux-observable/redux-observable
redux-saga
と同様、高度な非同期処理が実現できます。
非同期処理を実現するに当たってRxJS
に依存しています。
どれを使うべきか?
よほど複雑な要件がない限りはredux-thunk
で問題ないと思います。
(確かに連続した非同期処理が多い場合などは筋力が必要になりますが...)
公式ガイドラインでも述べられてますが、redux-thunk
とredux-sage
, redux-observable
は組み合わせて利用しても良いものです。
まずはシンプルなredux-thunk
から初めてみて、必要に応じてredux-sage
もしくはredux-observable
を導入するという手法が良いかもしれません。
参考記事
- reduxで非同期処理をするいくつかの方法
https://qiita.com/muijp/items/63386fd65c7e9f06f5d4#redux-saga
ルーティング
Reactでルーティングを実現する場合はreact-router
(react-router-dom
)というライブラリを利用するのが一般的です。
このreact-router
をReduxに統合する(例えばactionのdispatchで画面遷移を行う)ために、いくつかのライブラリが存在します。
ライブラリ候補
-
connected-react-router
https://github.com/supasate/connected-react-router
React Router v4 and v5に対応したライブラリ。
react-router
のAPIはそのままに、historyオブジェクトを強化し、その変更をstoreの状態に常に同期させることが可能となります。 -
redux-first-router
https://github.com/faceyspacey/redux-first-router
connected-react-router
と同じく、ルーティング情報をstoreの状態として管理します。
特徴的なのはURLの変化とstoreに保持しているルーティング情報が双方向バインディングされる点です。
通常はURLを書き換えることによってルーティング情報が書き換わりますが、ルーティング情報を書き換えることによってURLを更新することが可能となります。 -
react-router-redux
以前は広く利用されていたライブラリですが、React Router v4に対応しておらず、deprecatedとなりました。
古い書籍や記事ではこのライブラリが紹介されていることが多々ありますのでご注意ください。
どれを使うべきか?
正直redux-first-router
についてはそこまで触ってませんが、特に理由がない限りはconnected-react-router
で良いのではないでしょうか。
Githubのスター数を比較してもconnected-react-router
が広く利用されていることがわかります。
https://www.npmtrends.com/connected-react-router-vs-react-router-redux-vs-redux-first-router
UIコンポーネント
Reactに特化したコンポーネントセットは数多くのライブラリで提供されています。
ライブラリ候補
他のライブラリについては、先人の優れた比較記事があるため割愛します。
https://qiita.com/kyrieleison/items/39ce30dd2d204791a9ea
どれを使うべきか?
今回は、Google Material Design のガイドラインに沿った React コンポーネントセットである Material-UI
を採用しました。
Material-UI
のサイトを見れば一目瞭然ですが、基本的な画面部品についてはほとんど網羅されており、ドキュメントも非常に充実しています。
個別に導入したコンポーネントとしては、無限スクロールテーブルにおけるレンダリングを効率化するために導入した [react-virtualized]
(https://github.com/bvaughn/react-virtualized)ぐらいでしょうか。
筆者のチームでは Material-UI
をラップする形で、デザイン要件や個々の機能要件を反映したオリジナルコンポーネントを作成し、各開発者に利用させています。
国際化対応
react-intlを採用いたしました。
詳細は後日記載予定。
CSSスタイリング
Styled Componentを採用いたしました。
詳細は後日記載予定。
後日記載予定です。
ディレクトリ構成
開発を始めるにあたって必ずと言っていいほど悩むのがディレクトリ構成ではないでしょうか。
Redux自体は単なる状態管理のフレームワークのため、あらかじめ決められたディレクトリ構成は存在しません。
ただしRedux公式のガイドラインでいくつか一般的なディレクトリ構成が紹介されています。
https://redux.js.org/faq/code-structure#what-should-my-file-structure-look-like-how-should-i-group-my-action-creators-and-reducers-in-my-project-where-should-my-selectors-go
-
Rails Style
Redux公式のチュートリアルでも採用されている最もスタンダードな構成。
Railsのように構成要素(actions, reducers, containers, components, etc...)に応じてフォルダを分割するスタイル。 -
Domain Style
機能やドメインに応じてフォルダを分割し、その配下に action や reducer を格納していくスタイル。 -
Ducks
https://github.com/erikras/ducks-modular-redux
Domain Styleに似ているが、action, action type, reducerを1ファイルにまとめるスタイル。 -
re-ducks
https://github.com/alexnm/re-ducks
公式のガイドラインでは紹介されていませんが、Ducksを改良したre-ducks
と言われるスタイルもあります。
最終的なディレクトリ構成
どのスタイルが正解ということはありません。
今回はRedux初学者が多いということもあり、最もスタンダードなRails Styleを採用しました。
書籍やインターネット上の記事でも最も採用されているスタイルであり、開発者が調べた時に無駄に混乱しないというのは重要なことだと考えています。
重要なのは、actionとreducerを分離して考えるべきではないということです。
あるディレクトリで定義されたreducerが、別のディレクトリで定義されたactionに反応することは可能であり、むしろそうあるべきだと言われています。
最終的なディレクトリ構成は下記のようになりました。
各ディレクトリごとに何をどういった単位で格納するか見ていきましょう。
src
├── actions # Action群
│ ├── apis
│ ├── core
│ └── pages
├── apis # Ajax通信を行うAPIクラス群
├── common
├── components # Presentational Component群
│ ├── atoms
│ ├── molecules
│ ├── organisms
│ ├── pages
│ └── templates
├── containers # Container Component群
│ ├── molecules
│ └── pages
├── reducers # Reducer群
│ ├── core
│ └── pages
├── selectors # Selector群
│ ├── common
│ └── pages
└── validators # Validator群
├── common
└── pages
各構成要素の単位と責務
構成単位を検討する上で前提となるポリシーですが、筆者のチームでは1画面単位に各種要素(Action, Reducer, Selector) を設ける方針としています。
画面単位ではなくドメイン駆動的に構成することで、より再利用性の高い構成も考えられるかもしれませんが、画面単位で開発者に開発タスクを割り振る進め方をしていたこともあり(また初心者に対するわかりやすさの考慮もあり)このような方針としています。
components
Presentational Component と呼ばれる、見た目だけを扱うReduxに依存しないコンポーネントを格納します。
このコンポーネントは原則 状態を保持・操作せず、 後述する Container Component から渡されたプロパティを表示することに専念します。
構成単位
1ファイル / 1部品, 1ファイル / 1画面となります。
React におけるコンポーネントは「再利用」可能なものであり、筆者のチームでは Atomic Design に従ってサブディレクトリを切って各コンポーネントを分類しています。
Atomic Design の詳細については割愛しますが、コンポーネントを5つの単位に分割する考え方となります。
components
├── atoms # UIを構成する最小単位となるような基本的な要素 ex. ボタン, テキストボックス
├── molecules # Atomsを組み合わせて作るシンプルな要素 ex. ラベル付きテキストボックス
├── organisms # AtomsやMolecules、また他のOrganismsを組み合わせて作る複雑な要素 ex. ヘッダー, フッター
├── templates # MoleculesやOrganismsを組み合わせて作るページの構造 (ワイヤフレーム)
└── pages # Template内に実際の文章や画像などが入った実際のページ
├── XXXPageComponent.ts
├── YYYPageComponent.ts
...
Atoms, Molecules, Organisms が再利用可能な共通部品であり、Pages が個別画面となります。
実装上のポイント
-
可読性及びライフサイクル利用の観点から、Presentational Component は Functional Component ではなく Class Component としての作成を原則としています。
-
先に、コンポーネントは原則状態を保持しないと述べましたが、筆者のチームでは共通コンポーネントに関してのみはReduxの状態管理を利用せず、コンポーネント内で状態を保持する方針としています。
というのも共通部品というのは1画面内で複数個利用され得るものであり、Reduxのstateでは状態を別々に管理するのが困難なためです。
これについては下記でも議論されています。
containers
Container Component と呼ばれる、Redux の store や action を受け取り、Presentational Component に対して具体的なデータや振る舞い(コールバック関数)を引き渡すコンポーネントです。
このコンポーネントのコードは非常に明確で、 mapStateToProps
, mapDispatchToProps
, connect
を利用してReduxの状態や振る舞いを定義するのみとなります。
構成単位
1ファイル / 1画面となります。
components
└── pages # Template内に実際の文章や画像などが入った実際のページ
├── XXXPageContainer.ts
├── YYYPageContainer.ts
...
apis
HTTP通信を行う React, Reduxに依存しないクラスを格納します。
今回HTTP通信にはaxiosを利用しました。
構成単位
1ファイル / 1APIとなります。
1ファイル内でリクエストクラス、レスポンスクラスも定義しています。
apis
├── getUserApi.ts
├── postUserApi.ts
...
実装上のポイント
-
tysonと呼ばれるライブラリを利用することで、Decoratorを利用してJSONのシリアライズ、デシリアライズを行っています。
これによりキャメルケース <-> スネークケースの変換が実現できるだけでなく、APIとクライアント側のソースコードをより疎結合にし、APIのレスポンス名が変わるといった外部仕様の変更にも最小限の修正で対応できるようにしています。
actions
Action は store の state を変更するためのメッセージです。
Action 及び Action Creator(同期, 非同期)を格納します。
構成単位
1ファイル / 1画面, 1ファイル / 1API となります。
actions
├── core # 個別画面やAPIに依存しない共通Action
├── apis # API単位のAction
│ ├── getUserAction.ts
│ ├── postUserAction.ts
│ ...
└── pages # 画面単位のAction
├── XXXPageAction.ts
├── YYYPageAction.ts
...
実装上のポイント
- Actionオブジェクトの構造は Flux Standard Action(FSA)に従って定義します。
- API Action に関しては、開始時(START)、成功時(SUCCESS)、失敗時(FAILURE)、完了時(COMPLETE)の 4Action を定義しています。
- 画面単位の Action は関連する Component からのみ dispatch されるのに対し、 API単位の Action は画面横断的に再利用されます。
reducers
state を変化させる為の関数となる reducer を格納します。
reducer の責務は非常に明確で、 action をハンドリングして state を変更することとなります。
構成単位
1ファイル / 1画面 となります。
reducers
├── core # 個別画面に依存しない共通Reducer
├── pages # 画面単位のAction
│ ├── XXXPageReducer.ts
│ ├── YYYPageReducer.ts
実装上のポイント
- Reducerでは状態(データ)の計算や加工は極力行わず、生データを保持するようにします。
例えばAPIコール時のレスポンスを画面表示用に加工するのは Component や Selector の責務であり、state としては汎用的なデータを保持しておく必要があるという考え方に基づいています。 - reducer は冪等であることが非常に重要であり、次のようなことをしてはいけません。
- 引数を変更してはいけない。
- APIコールやルーティングのような副作用のある処理を行ってはいけない。
-
Date.now()
やMath.random()
といった純粋関数ではない関数を呼び出してはいけない。
selectors
Reselectというライブラリを利用して、Redux の state を部分的に監視し、当該の state に変更があった場合のみ計算結果を返却する(Memoizationといわれます)ような関数を作成することができます。
このような関数を Selector と呼びます。詳細については本家のガイドラインや参考記事を参照してください。
- Redux公式
https://redux.js.org/recipes/computing-derived-data - Reduxのreselectとは
https://qiita.com/zaki-yama/items/5258e6f1ae37f63034b9
筆者のチームでは Selector の責務を下記の2つと定めて作成しています。
- 画面表示用のデータの演算やフィルタリング, ソートといった計算を行う。
- バリデーションを行う。(後述)
構成単位
1ファイル / 1画面となります。
selectors
├── common # 個別画面に依存しない共通セレクタ
├── pages # 画面単位のセレクタ
├── XXXPage.ts
├── YYYPage.ts
実装上のポイント
- 計算処理が必要ない場合でも将来的な拡張に備え、原則 Selector を作成し、 Container Component は Selector 経由で state を取得します。
- パフォーマンスを考慮して、計算処理は可能な限り Selectorに寄せます。
ただし Presentational Component で計算を行わなければならない状況もあり、このあたりは柔軟に対応します。
悩ましい問題
APIコール時のリクエストをどこから渡すか
検索条件は全てRedux の state で管理されている前提となります。
- Container Component 内の
mergeProps
を利用して引き渡す - Presential Component 内で引き渡す
- Action Creator内で
redux-thunk
のgetState()
を利用して引き渡す
ここでポイントとなるのは、全てのリクエストが画面表示に利用されるわけではないという点です。
このため案2の方式では、画面表示に利用しない綱目を props として引き渡すことになり、これは望ましくありません。
筆者のチームでは最終的には 3.Action Creator の方式を採用しました。
ただし getState()
を多様するのはアンチパターンだとする記事もあり、この辺りは色々な考え方がありそうです。
https://itnext.io/the-perils-of-using-a-common-redux-anti-pattern-344d778e59da
筆者自身Actionを肥大化させることは本来の考え方と異なるなと思う部分もありますが、redux-thunk
を利用している以上割り切ってもいい部分なのかなと考えています。
バリデーションをどこで行うか
ここでのバリデーションとは、フォームの入力に対して文字数や最小値、最大値といったチェックを行うことで、エラーの状態を管理し、スタイルを変更したり、メッセージを表示したりすることを指します。
下記の記事ではいくつかの方法が紹介されていますが、Reduxにおいてどこでバリデーションを行うべきかというのは明確に定義されておらず、あまりデファクトとなっている手法も存在しないように見受けられます。
https://qiita.com/terrierscript/items/5bed7812b3c1447b7b60
- Presentational Component で行う
- Action で行う
- Reducer で行う
- Middleware で行なう
- Selector で行なう
-
redux-form
のようなライブラリを導入して行なう
筆者のチームでは下記の考慮から 5.Selectorで行う 方式を採用しました。
- Viewに特化したPresentational Component をロジックで肥大化させたくない。
- Action や Reducer でバリデーションを行うのはその責務を超えており、全ての入力部品のエラー状態を各Reducer で保持するのは煩雑である。
-
redux-form
がやや複雑でキャッチアップコストもかかる。
Selectorでバリデーションを実装する方法については下記の記事が非常に参考になりました。
- https://qiita.com/notsunohito/items/76d912c5e266670f2662
- https://qiita.com/notsunohito/items/a3a8324b40bbaea4bf9a
複数のActionの発行をどこで束ねるか
例えば 「検索条件のクリア」「検索結果のクリア」「検索APIコール」というようなアクションを順番に発行したいケースを考えてみてください。
-
redux-thunk
を利用して複数のActionを dispatch する Action Creator を作成する - 全てが同期処理の場合は、複数のActionを 単一Actionとして定義して Reducer 側に制御を任せる
- Container Component 側で複数 Action の dispatch を束ねた関数をPresentational Componentに渡す
これは案1が望ましいと考えます。
案2の場合は Action の再利用性が低下してしまい、案3の場合は state へのアクセスしたい場合や非同期処理をチェーンさせたい場合に煩雑になってしまいます。
筆者のチームでは案1と案3の実装が混在していたので案1に寄せました。
最後に
まだまだ書ききれない内容も多く、コードサンプル含め継続的に記事をアップデートしていく所存です。
少しでもこれから開発を始める方の参考になれば幸いです。