Table of Contents
2018年後半にスクラッチから作ったReactとReduxのプロジェクトテンプレートを2020年版として色々アップデートしているなかで、re-ducksパターンに則ってステート管理のモジュール構成を整理しなおしたり、ステート管理に使うライブラリを見直したりした。
この記事では、Reduxのおさらいをちょっと濃いめにしつつre-ducksパターンについて説明し、次のre-ducsパターンの実践の記事につなげる。
Reduxのおさらい
ReduxはFluxというアーキテクチャに従った状態管理ライブラリで、単一のオブジェクトでアプリケーション全体の状態(State)を表し、これをStoreというオブジェクトに保持する。
Storeに対してActionをdispatchする、つまりStore.dispatch(action)
を呼ぶことで、Storeに登録されたReducerが、現在のStateとdispatchされたActionから新しいStateを計算し、現在のStateを置き換える。
Storeをsubscribeしてコールバックを登録しておくと、StoreへのActionのdispatchのたびに(Reducerの実行後に)そのコールバックを呼んでくれる。
コールバックでは何も受け取れないんだけど、コールバックのなかでStore.getState()
を呼ぶことで、Reducerが更新したStateオブジェクトを取得できる。
このsubscribeとかコールバックの処理はReact-Reduxがやってくれる。(下図get State
とsubscribe
の部分。)
get State
とsubscribe
はReact-ReduxのuseSelector()というHookで実装されている。
また、get dispatch func
はStoreのdispatch関数オブジェクトを取得する処理を示していて、React-ReduxのuseDispatch()というHookの呼び出しにあたる。
Container ComponentがuseSelector()
でState(の一部)を取得して整形して、さらにuseDispatch()
取得したdispatch
を使うイベントハンドラを作って、それらをPropsとしてPresentational Componentに渡してrender
する。
Presentational Componentはユーザの操作に応じてそのイベントハンドラを呼んで、Actionをdispatchする。
というのがReactのViewとReduxのStateをつなぐ基本形。
dispatch()
の呼び出しから、Reducerの実行、コールバックの呼び出し、Stateオブジェクトの取得までは同期的逐次的に実行され、React-ReduxはStateオブジェクトの変更を検知すると、Container Componentの再renderが必要であることをReactに伝える。
render処理とReducerはピュアである必要があるので、REST API呼び出しなどの副作用は別のところに書かないといけない。 副作用はuseEffectとかイベントハンドラに書くことはできるけど、Reduxを使う場合はRedux Sagaなどのミドルウェアを使って書くとView側をdumb目に保てていい。
Redux Sagaを使う場合、Sagaと呼ばれるジェネレータ関数を定義して、Actionの取得、副作用の実行、Actionのdispatchといった処理を書くことができる。
Sagaは、特定のActionについてStoreへのdispatchをwatchし、そのActionをReducerの実行やコールバックの呼び出しの後に取得できる。
関連する過去の記事:
- https://www.kaitoy.xyz/2018/09/26/creating-react-redux-app-from-scratch-06/
- https://www.kaitoy.xyz/2018/10/01/creating-react-redux-app-from-scratch-07/
- React-Reduxの古いほうのAPIである
connect()
を使っている。
- React-Reduxの古いほうのAPIである
- https://www.kaitoy.xyz/2018/10/07/creating-react-redux-app-from-scratch-08/
re-ducksパターン
前節の図の「View」で囲われた部分のモジュール構成については以前の記事に書いていて、それ以外の部分、つまりステート管理系のモジュール構成がこの記事の本題。 その部分には以下のような実装が含まれることになる。
- Actionの定義や生成処理
- Reducer
- Storeの生成処理
- Saga
- etc.
これらを実装するモジュールは、古くは以下のようにその種別ごとに整理する方法(Rails-style)が主流だった。
- src/
- actions/
- userActions.ts
- articleActions.ts
- reducers/
- userReducers.ts
- articleReducers.ts
- sagas/
- store/
- …
- actions/
これだと、ひとつの(ドメイン)モデルに関する処理が複数のディレクトリに散らばってしまうし、どのモジュールが何に対する責務を負うのかが曖昧になっていきがち。 昨今のDDDやマイクロサービスアーキテクチャといったトレンドに共通的な考え方として、コンポーネント種別とかレイヤでまとめるより、ドメインやドメインモデルにそってまとめるべきというのがあると思うのだけど、ステート管理のモジュールアーキテクチャにも同じような考え方のものがある。
それがre-ducksパターンで、以下のような形になる。
- src/
- state/
- ducks/
- user/
- actions.ts
- reducers.ts
- sagas.ts
- …
- article/
- actions.ts
- reducers.ts
- sagas.ts
- …
- user/
- ducks/
- state/
re-ducksのduckというのはアヒルで、ドメインモデルのメタファになっているのだと思う。(ref. duck typing) もちろんReduxとかけてもいる。 要は、ducksディレクトリの下にモデルごとのディレクトリを作って、その中にそのモデルに関連する各種ステート管理モジュールを詰め込んでまとめるという形。 まとめたものや、それに対応するモデルをduckと呼ぶ。
re-ducksにすることで、強く関連するモジュールが一か所にまとまるし、それぞれのモジュールの責務がより明確になるので保守性が高くなる。
ステートの構造もモデルごとにネームスペースを分けるのが原則なので、ディレクトリ構造とステート構造が自然と対応する感じになって分かりやすい。
srcディレクトリの下に一段stateディレクトリを設けているのは、re-ducksとは直接関係ないけど次のような意図がある。
Reduxのスタイルガイドにはステート用モジュールとView用モジュールを一緒くたにするディレクトリ構造例が載ってるけど、Viewは必ずしもステート(というかモデル)をそのまま投影するものでもないので、分けておいたほうがいい。
分けておくことでステートをViewから独立した処理系ととらえる意識を保てて、可読性とテスタビリティを確保しやすい。
ということで、Viewのコード(i.e. tsxファイル)はsrc/views/
に分けて入れられるように、stateディレクトリを一段掘っている。
以前の記事で書いたAtomicデザインを反映したViewのディレクトリ構造を合わせると以下のようになる。
- src/
- views/
- atoms/
- buttons/
- OKButton.tsx
- CloseButton.tsx
- FormContainer.tsx
- buttons/
- molecules/
- organisms/
- user/
- UserFormContents.tsx
- article/
- user/
- ecosystems/
- user/
- UserForm.tsx
- article/
- user/
- natures/
- user/
- UserFormView.tsx
- article/
- ArticleListView.tsx
- user/
- hooks/
- atoms/
- state/
- store.ts
- ducks/
- index.ts
- app/
- ui/
- user/
- index.ts
- actions.ts
- apis.ts
- reducers.ts
- models.ts
- sagas.ts
- selectors.ts
- watcherSagas.ts
- article/
- utils/
- views/
これらのモジュールの中身をどのように書くかについては次回書く。