はじめに
ずっとReduxから逃げて他のstate管理ライブラリを使っていたんですが、ついに捕まったので調査しました。
Reduxの全体像
要素 | 役割 |
---|---|
Component | Storeを利用するコンポーネント。ComponentはUIにのみ責任を持ち、ロジックはContainerに分離する。 |
Action | Storeに対して、stateの参照や変更を依頼するための依頼書。ComponentがActionCreatorを通して作成する。 |
ActionCreator | Componentから必要な情報を受け取り、Actionを作成する |
Container | ComponentとStoreを接続し、Componentに対してstateとdispatch()を提供する。stateに対して何がしかの処理を加えたい場合、Container内にロジックを記述する。 |
dispatch() | actionをReducerに届け、処理を促す。 |
Store | データストア。stateを集中管理する。Reducer以外からのstate変更を受け付けない。 |
Reducer | Actionを受け取り、Store内のstateに変更を及ぼす。stateの種類によってReducerを分けるのが一般的。 |
CombineReducer | 複数あるReducerをひとまとめにし、Storeと接続する。 |
ユーザアクションに応じて、Store内のstateを更新する際のデータフロー
下記流れでStore内のstateを更新します。
- Componentがイベントを検知し、Actionを作成する。
- 作成したActionを、Containerから提供されたdispatch()を用いて、Reducerに処理を依頼する。
- Reducerは、現在のstateとActionをもとに新たなstateを計算し、そのstateをStoreに反映させる。
Componentがイベントを検知し、Actionを作成する。
Componentはイベント(ユーザのクリックとか)を検知して、ActionCreatorを通じてActionを作成します。
この時、ComponentはActionCreatorに対して、下記の情報を提供します。
Type | アクションの種類を識別するための文字列 |
Payload | アクションに必要なデータ(引数) |
ActionCreatorは上記をもとに、Actionを作成します。
Actionの例↓
{
type: "todos/todoAdded",
payload: todoText,
}
作成したActionを、Containerから提供されたdispatch()を用いて、Reducerに処理を依頼する。
ComponentはActionをDispatchして、Reducerに処理を依頼する。
dispatch({ type: 'todos/todoAdded', payload: 'todoText' })
Reducerの中身はこんな感じ↓
// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
case 'todos/todoAdded': {
// We need to return a new state object
return {
// that has all the existing state data
...state,
// but has a new array for the `todos` field
todos: [
// with all of the old todos
...state.todos,
// and the new todo object
{
// Use an auto-incrementing numeric ID for this example
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}
ActionのTypeを参照して、処理を分岐させてる。
また、Action.payloadを参照することで、Componentが引数として指定した任意のデータも参照することができる。
Reducerは、現在のstateとActionをもとに新たなstateを計算し、そのstateをStoreに反映させる。
実際に新たなstateを反映しているのは、下記のreturn文。
case 'todos/todoAdded': {
// We need to return a new state object
return {
// that has all the existing state data
...state,
// but has a new array for the `todos` field
todos: [
// with all of the old todos
...state.todos,
// and the new todo object
{
// Use an auto-incrementing numeric ID for this example
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
補足)ContainerとComponent
Containerの役割はStoreとComponentの橋渡し。
しかし、ComponentはContainerがいなくても、直接Storeのdispatch()を利用することができる。
なぜContainerが必要か?
ComponentがReduxに依存してしまう。
⇒
- Componentが状態やロジックを保持することになり、テストがしにくくなる。
- Redux以外のデータソースから、Componentに対して値を受け渡しにくくなる。
ちなみに、Containerは下記のように、Componentに対してStateとdispatch()を提供する。
function mapStateToProps (state) {
return {...state};
}
function mapDispatchToProps(dispatch) {
return {
todoAdded: () => dispatch(todoAdded()),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Component);
※上記のconnect()は古いらしく、hook全盛期のReactではもっとシンプルに書けるそうです。
https://qiita.com/seya/items/700184c0d4a52bc0d32b
間違いあればご指摘ください!
追記
Container内で、dispatch()をカスタマイズ(任意のActionを定義)する関数を作成し、Componentはその関数に対してPayloadを投げてあげる。みたいな構成もあるらしい。
(そっちの方がAction単位でテストできるし、Containerの肥大化も避けれるしよさげ??)
参考
https://redux.js.org/tutorials/essentials/part-1-overview-concepts
https://qiita.com/mpyw/items/a816c6380219b1d5a3bf#action-%E3%81%8A%E3%82%88%E3%81%B3-action-creator
https://qiita.com/seya/items/700184c0d4a52bc0d32b
https://react-redux.js.org/api/hooks