【追記】
もうこれ古いから参考にしないでください https://t.co/mXtcc73Orf
— もし Laravel が流行しなくなってこられてきてたとしたら、絶対に捨てられてこられてたと思うか (@mpyw) January 26, 2021
Redux にはその昔 connect()() とかいうクソ API と, Redux-Saga とかいう宗教がありました
— もし Laravel が流行しなくなってこられてきてたとしたら、絶対に捨てられてこられてたと思うか (@mpyw) January 26, 2021
という考古学です
読者対象
「チュートリアルそれぞれ一周した!Reactは何とか理解できたが,Reduxがさっぱりわかんねぇ!」
ぐらいの人向け。自分もまだ未熟なので理解の確認のために書いた。
「React」と「React+Redux」の決定的な違い
- React単体の場合,Reactコンポーネント自身が個別に状態管理をする。
- React+Reduxの場合,**状態管理する専用の場所(ストア)**で状態管理し,Reactコンポーネントはそれを映すだけに徹する。
Redux図解
フロー全体図
Store
状態は基本的に全てここで集中管理される。イメージとしては**「でっかいJSONの塊」**。
{
value: 0,
}
規模が大きい場合は状態をカテゴリ別に分類するのが一般的である。
{
// セッションに関するもの
session: {
loggedIn: true,
user: {
id: "114514",
screenName: "@mpyw",
},
},
// 表示中のタイムラインに関するもの
timeline: {
type: "home",
statuses: [
{id: 1, screenName: "@mpyw", text: "hello"},
{id: 2, screenName: "@mpyw", text: "bye"},
],
},
// 通知に関するもの
notification: [],
}
Action および Action Creator
Storeおよびそこに存在するStateはとても神聖なものだ。Reactコンポーネントなんぞ下界のものに直接触らせるわけにはいかんのだ。ここに触れるためには,Actionという儀式を介さなければならない。要するにイベント・ドリブンと同じ概念だ。
- Storeに対して何かしたい奴はActionを発行する
- Storeの門番がActionの発生を検知すると,Stateが更新される
Actionは基本的に以下のようなフォーマットを持つオブジェクトになる。
{
type: "アクションの種類を一意に識別できる文字列またはシンボル",
payload: "アクションの実行に必要な任意のデータ",
}
例えば,カウンタの値を2増加させたい場合,以下のようなオブジェクトになるだろう。頭に @@myapp/
とプレフィックスをつけたのは,他の人が書いたコードとの衝突を避けるためだ。
{
type: "@@myapp/ADD_VALUE",
payload: 2,
}
ところで,いちいちこんなオブジェクトを作るのを手作業で書かせるのもつらい。また,"@@myapp/ADD_VALUE"
のように毎回アクション名を文字列で書くのも嫌な感じがする。そこで,これを少し楽にするための定数と関数を用意するのが一般的である。外部ファイルから参照する必要性に備えて,ちゃんとexportもしておこう。
export const ADD_VALUE = '@@myapp/ADD_VALUE';
export const addValue = amount => ({type: ADD_VALUE, payload: amount});
Reducer
先ほど「Storeの門番」と書いたが,それに相当する役目を背負っているのがこいつだ。
関数型プログラミングにおいて,Reduceという用語は畳み込み演算を意味する。Reduxにおいては,以下のように,以前の状態とアクションを組み合わせて,新しい状態を生み出すという操作になる。
import { ADD_VALUE } from './actions';
export default (state = {value: 0}, action) => {
switch (action.type) {
case ADD_VALUE:
return { ...state, value: state.value + action.payload };
default:
return state;
}
}
※ 2018年4月現在 Object rest spread transform 必須の記法が含まれています。このコードを使用される場合は各自プラグインをインストールしてください。
注意して見るべきは以下の2点。
- 初期状態はReducerのデフォルト引数で定義される
- 状態を変更する際,渡されてきた
state
そのものを書き換えずに,新しいものを合成するように書く
さて,こいつがリターンしたstate
は,ストアにすぐ反映され,以下のように変化する。
{
value: 2,
}
Twitterの例のように,大規模な開発においてReducerを細かく分割する場合は,Reduxによって提供されるcombineReducers
という関数を用いて,以下のように書く。
import { combineReducers } from 'redux';
const sessionReducer = (state = {loggedIn: false, user: null}, action) => {
/* 省略 */
};
const timelineReducer = (state = {type: "home", statuses: []}, action) => {
/* 省略 */
};
const notificationReducer = (state = [], action) => {
/* 省略 */
};
export default combineReducers({
session: sessionReducer,
timeline: timelineReducer,
notification: notificationReducer,
})
こうすると,Reducer分割に使われたキーがそのままState分割にも流用される。なお実際には,それぞれのReducerの定義自体も別ファイルに分けるのが普通だ。
純粋なComponentと接続されたComponent
ReactのComponent単体では,Reduxの流れに乗ることはできない。参加するためには,ReactReduxによって提供されるconnect
という関数を用いて,以下のように書く。関数版とクラス版それぞれを示す。
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addValue } from './actions';
const Counter = ({ value, dispatchAddValue }) => (
<div>
Value: {value}
<a href="#" onClick={e => dispatchAddValue(1)}>+1</a>
<a href="#" onClick={e => dispatchAddValue(2)}>+2</a>
</div>
);
export default connect(
state => ({ value: state.value }),
dispatch => ({ dispatchAddValue: amount => dispatch(addValue(amount)) })
)(Counter)
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addValue } from './actions';
class Counter extends Component {
render() {
const { value, dispatchAddValue } = this.props;
return (
<div>
Value: {value}
<a href="#" onClick={e => dispatchAddValue(1)}>+1</a>
<a href="#" onClick={e => dispatchAddValue(2)}>+2</a>
</div>
);
}
}
export default connect(
state => ({ value: state.value }),
dispatch => ({ dispatchAddValue: amount => dispatch(addValue(amount)) })
)(Counter)
さあ,ここで途端に図式が複雑になる気がする。自分がReduxを勉強している最中に,最も理解に時間がかかった部分がここだ。分かってしまえば大したことないので,焦らず冷静に見ていこう。
まず,ComponentがStoreから何か情報を受け取る場合,それはprops
を通じて渡されてくる。props
はイミュータブルだ。状態が更新されるたびに新しくprops
が作り直され,その都度render
メソッドが実行される。これを踏まえた上で,connect
を実行している周囲のコードを見てみよう。
- Store が持つ状態
state
をどのようにprops
に混ぜ込むかを決める
(この動作を定義する関数はmapStateToProps
と呼ばれる) - Reducer にアクションを通知する関数
dispatch
をどのようにprops
に混ぜ込むかを決める
(この動作を定義する関数はmapDispatchToProps
と呼ばれる) - 上記2点が混ぜ込まれた
props
を受け取りたい Component を決める - Store や Reducer と繋がれるように細工された Component が返り値になる
connect(mapStateToProps, mapDispatchToProps)(Component)
という書き方には独特の雰囲気があるが,最終的な返り値は4のようなものである。以下では,mapStateToProps
とmapDispatchToProps
についてさらに解説する。
mapStateToProps
引数として渡されてくるstate
は全体を意味することに注意されたい。カウンタの例では
{
value: 2,
}
が
<Counter value={2} />
と入ることを期待して state => ({ value: state.value })
と書いた。今回の場合は他に邪魔なプロパティが一切存在しないので state => state
と書いても動作するが,基本的に必要なものだけを選別してprops
に混ぜ込むのが普通だ,ということを覚えておこう。
mapDispatchToProps
Action Creator でアクションを作っても,それだけでは何も起こらない。Reducerに向けて通知するには,作ったアクションを,ここで渡されてくるdispatch
という関数にさらに渡さなければならない。
こうすると,すべてのReducerが実行される。Reducer にswitch文での分岐が書かれていたのはこのためである。Reducer は,関係無いアクションは無視して,自分宛てのアクションだけを処理するように書かなければならない。
また,コンポーネント側でいちいち手動でdispatch
する処理を書かなくて良いように,ここでアクションの生成からdispatch
の実行までを一気に行う関数を定義してprops
に渡してあげよう,というのがこいつの主な存在意義である。
bindActionCreators
なんと,mapDispatchToProps
で上記のようなコードを書くことさえサボることのできる,bindActionCreators
という関数も提供されている。これを使えば以下のような省略が可能だ。
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { addValue } from './actions';
const Counter = ({ value, addValue }) => (
<div>
Value: {value}
<a href="#" onClick={e => addValue(1)}>+1</a>
<a href="#" onClick={e => addValue(2)}>+2</a>
</div>
);
export default connect(
state => ({ value: state.value }),
dispatch => bindActionCreators({ addValue }, dispatch)
)(Counter)
2017/06/25 追記: 現在はbindActionCreators
の実行も省略することができる。
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addValue } from './actions';
const Counter = ({ value, addValue }) => (
<div>
Value: {value}
<a href="#" onClick={e => addValue(1)}>+1</a>
<a href="#" onClick={e => addValue(2)}>+2</a>
</div>
);
export default connect(
state => ({ value: state.value }),
{ addValue }
)(Counter)
Container
今回は単に「接続されたComponent」と呼んだが,場合によっては「Container」と呼ばれるべきComponentも出てくる。以下のようなものが該当する。
- たくさんのComponentがリスト形式で集められているが,各要素のComponent各々で接続すると収拾がつかなくなるので,代表して子要素を抱えるだけの1つの親Componentがconnectされにいく
(<UsersList>
<User />
<User />
<User />
<User />
</UsersList>)
この代表してconnectされにいく親ComponentのことをContainerと呼ぶ。Containerは可読性を高めるために,Componentとはディレクトリが分けられることが多い。
理解度チェック
ここまで一通り理解すれば,あとは画像を見返すだけで大まかに「Reduxがどんな動きをするか」は思い出せると思う。
発展
Redux Saga の導入
実はReact+Reduxだけでは,まだ困ることがある。
- Reducerの中には副作用を生む処理を書いてはいけない
という原則があるからだ。
- 同じ入力に対して確率的に結果が変化する処理
- 遅延処理
- HTTPリクエスト処理
こういったものは基本的にReducerの中には書いてはいけない。ではいったいどこに書くべきなのか?
- Component の中
- Action Creator の中
-
mapDispatchToProps
の中
いやいや,これらは全部アンチパターンだ。正しい答えは Saga だ。
今までは, connectされたComponentからアクションがdispatch
されると,それはReducerに向かうだけ,と決まっていた。ここに新しい受け手を用意するのが Saga である。
Sagaは全てジェネレータ関数であるため,非同期処理を簡単に捌くことができる。
yield take(ACTION_TYPE)
で指定したアクションの発生を監視する- 取ってきたアクションで煮るなり焼くなりする
yield put(action)
で結果をまた別のアクションとして排出する
基本はこれだけだ。排出したアクションはReducerに向かうこともあるし,自分自身のSagaにもう一度流入してくるかもしれないし,自分以外の別のSagaに流入していくかもしれない。
上の記事が非常に秀逸なので,Sagaについて詳しく知りたい方は是非とも一読を。