たぶんこれが一番分かりやすいと思います React + Redux のフロー図解

  • 989
    Like
  • 5
    Comment

読者対象

「チュートリアルそれぞれ一周した!Reactは何とか理解できたが,Reduxがさっぱりわかんねぇ!

ぐらいの人向け。自分もまだ未熟なので理解の確認のために書いた。

「React」と「React+Redux」の決定的な違い

  • React単体の場合,Reactコンポーネント自身が個別に状態管理をする。
  • React+Reduxの場合,状態管理する専用の場所(ストア)で状態管理し,Reactコンポーネントはそれを映すだけに徹する。

Redux図解

フロー全体図

ReactRedux-DataFlow.svg.png

Store

状態は基本的にすべて全てここで集中管理される。イメージとしては「でっかいJSONの塊」

例: カウンタ
{
    value: 0,
}

規模が大きい場合は状態をカテゴリ別に分類するのが一般的である。

例: TwitterがもしReduxを使っていたら
{
    // セッションに関するもの
    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という儀式を介さなければならない。要するにイベント・ドリブンと同じ概念だ。

  1. Storeに対して何かしたい奴はActionを発行する
  2. Storeの門番がActionの発生を検知すると,Stateが更新される

Actionは基本的に以下のようなフォーマットを持つオブジェクトになる。

{
    type: "アクションの種類を一意に識別できる文字列またはシンボル",
    payload: "アクションの実行に必要な任意のデータ",
}

例えば,カウンタの値を2増加させたい場合,以下のようなオブジェクトになるだろう。頭に @@myapp/ とプレフィックスをつけたのは,他の人が書いたコードとの衝突を避けるためだ。

{
    type: "@@myapp/ADD_VALUE",
    payload: 2,
}

ところで,いちいちこんなオブジェクトを作るのを手作業で書かせるのもつらい。また,"@@myapp/ADD_VALUE"のように毎回アクション名を文字列で書くのも嫌な感じがする。そこで,これを少し楽にするための定数と関数を用意するのが一般的である。外部ファイルから参照する必要性に備えて,ちゃんとexportもしておこう。

ReactRedux-Action.svg.png

actions.js
export const ADD_VALUE = '@@myapp/ADD_VALUE';
export const addValue = amount => ({type: ADD_VALUE, payload: amount});

Reducer

先ほど「Storeの門番」と書いたが,それに相当する役目を背負っているのがこいつだ。

関数型プログラミングにおいて,Reduceという用語は畳み込み演算を意味する。Reduxにおいては,以下のように,以前の状態とアクションを組み合わせて,新しい状態を生み出すという操作になる。

ReactRedux-Reducer.svg.png

reducer.js
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;
    }
}

注意して見るべきは以下の2点。

  • 初期状態はReducerのデフォルト引数で定義される
  • 状態を変更する際,渡されてきたstateそのものを書き換えずに,新しいものを合成するように書く

さて,こいつがリターンしたstateは,ストアにすぐ反映され,以下のように変化する。

{
    value: 2,
}

Twitterの例のように,大規模な開発においてReducerを細かく分割する場合は,Reduxによって提供されるcombineReducersという関数を用いて,以下のように書く。

import { combineReducers } from 'redux';

const sessionReducer = (state = {loggedIn: false, user: null}, payload) => {
    /* 省略 */
};
const timelineReducer = (state = {type: "home", statuses: []}, payload) => {
    /* 省略 */
};
const notificationReducer = (state = [], payload) => {
    /* 省略 */
};

export default combineReducers({
    session: sessionReducer,
    timeline: timelineReducer,
    notification: notificationReducer,
})

こうすると,Reducer分割に使われたキーがそのままState分割にも流用される。なお実際には,それぞれのReducerの定義自体も別ファイルに分けるのが普通だ。

純粋なComponentと接続されたComponent

ReactのComponent単体では,Reduxの流れに乗ることはできない。参加するためには,ReactReduxによって提供されるconnectという関数を用いて,以下のように書く。関数版とクラス版それぞれを示す。

ReactRedux-Component.svg.png

Counter.js (関数版)
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)
Counter.js (クラス版)
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はイミュータブルだ。つまりこれは,状態が更新されるたびに新しくComponentが作り直されるということを意味している。これを踏まえた上で,connectを実行している周囲のコードを見てみよう。

  1. Store が持つ状態stateをどのようにpropsに混ぜ込むかを決める
    (この動作を定義する関数はmapStateToPropsと呼ばれる)
  2. Reducer にアクションを通知する関数dispatchをどのようにpropsに混ぜ込むかを決める
    (この動作を定義する関数はmapDispatchToPropsと呼ばれる)
  3. 上記2点が混ぜ込まれたpropsを受け取りたい Component を決める
  4. Store や Reducer と繋がれるように細工された Component が返り値になる

connect(mapStateToProps, mapDispatchToProps)(Component) という書き方には独特の雰囲気があるが,最終的な返り値は4のようなものである。以下では,mapStateToPropsmapDispatchToPropsについてさらに解説する。

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という関数も提供されている。これを使えば以下のような省略が可能だ。

ReactRedux-BoundActionCreator.svg.png

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がどんな動きをするか」は思い出せると思う。

ReactRedux-DataFlow.svg.png

発展

Redux Saga の導入

実はReact+Reduxだけでは,まだ困ることがある。

  • Reducerの中には副作用を生む処理を書いてはいけない

という原則があるからだ。

  • 同じ入力に対して確率的に結果が変化する処理
  • 遅延処理
  • HTTPリクエスト処理

こういったものは基本的にReducerの中には書いてはいけない。ではいったいどこに書くべきなのか?

  • Component の中
  • Action Creator の中
  • mapDispatchToProps の中

いやいや,これらは全部アンチパターンだ。正しい答えは Saga だ。

ReactRedux-DataFlow-with-Saga.svg.png

今までは, connectされたComponentからアクションがdispatchされると,それはReducerに向かうだけ,と決まっていた。ここに新しい受け手を用意するのが Saga である。

Sagaは全てジェネレータ関数であるため,非同期処理を簡単に捌くことができる。

  1. yield take(ACTION_TYPE) で指定したアクションの発生を監視する
  2. 取ってきたアクションで煮るなり焼くなりする
  3. yield put(action) で結果をまた別のアクションとして排出する

基本はこれだけだ。排出したアクションはReducerに向かうこともあるし,自分自身のSagaにもう一度流入してくるかもしれないし,自分以外の別のSagaに流入していくかもしれない。

redux-sagaで非同期処理と戦う - Qiita

上の記事が非常に秀逸なので,Sagaについて詳しく知りたい方は是非とも一読を。