Redux

Redux の仕様を説明をするほど勉強した訳ではあらず、これを触ってみたい思いに駆られた不純な動機あたりからぼちぼち。

まず、React コミュニティでは、かなりあちこちで Redux という単語を見せつけられる。React Router あたりを勉強していると、Routerという単語よりもむしろ多く見るかもしれない。これをそのまま素通りするは武士の一分が立たぬ。

次に、Jest による Unit Test を書き始めたところ、対象のモジュールに singleton なモジュール絡むと(それを import して何かしらすると)、Testが走る前にJest の validate に引っかかり、Unit Test自体がエラーとなることに唖然とする。

例えばだが、"Loading..." などを modal に表示する spinner モジュール等は、どこかに置いてglobal に公開し、どのモジュールからでも使わせて良いではないか。複数の Ajax request が あちこちから、ほぼ同時に post される際にも、一か所で処理できれば、無駄な render() 実行が少なくなるだろう。なので、これを singleton として実装してきた。ところが、実動作は全くOKなのだが、Unit Test でsingleton は色々と問題が出る。となると、factory 作って、、、となるかもしれないが、Reduxというのが救世主なのでは無いかとぼんやり感じていたので、勉強してみた。見事に解決してくれたので、その備忘録。

---
ReduxをC#的に乱暴に表すと、イベント群を取り扱う小さな class であり、その目的は、あちこちのクラスに分散しているであろう events の起動やリスナーを一か所で集中管理する(これはC#で普通に実施している方も多いと思う)のだが、各クラスのインスタンスの state も同時に集中管理する、というのがみそだと思う。実際に、Redux を C#に取り込むというサイトもある。http://massivepixel.co/blog/post/redux-csharp-part1

Redux aware なモジュールは container と呼ぶそうだが、それは、a) 自己の state の変化を Redux に知らせ管理してもらう、b) または Redux から state の変化イベントに subscribe して、自己の render()等に反映させる、c) またはその両者を行うモジュールである。

本文の不純な動機の例では、あるモジュールで Ajax を起動しAPI からデータを貰ってくるのだが、async 処理の直前に spinner を表示し、データ受領が完了したらそれを消去(非表示)するというもの。

アプリの親分(rootモジュール)は、以下:

import * as ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import {createStore} from "redux";
import { Provider } from "react-redux";
import App from "./components/app";

const rstore = createStore(reducers);

ReactDOM.render((
    <Provider store={rstore} >
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </Provider>
), document.getElementById("root"));

react-redux を利用して <Provider>で囲むことにより各モジュール群(子、孫、ひ孫・・・)を Redux が使用可能な状態に置くことができる。使用する必要が無い場合は、なにも悪い副作用はない。使用する場合は、Redux (react-redux)が提供する極少ない API のうちの一つ、 connect() を実施し、当該モジュールの props と Redux を紐づける。紐づけさえ為されれば、Reduxに action を送ったり、Redux が管理している event に subscribe (props を通じて) し、変化をUIに反映可能となる。紐づけしたいモジュールの class の下に以下のようものを書けばよい。

const mapDispatchToProps = (dispatch: any) => ({
    setSpinner(value: boolean, text?: string) {
        dispatch(showSpinner(value, text));
    },
});

export default withRouter(connect(() => ({}), mapDispatchToProps)(Tasks));

(withRouterは本文と無関係だが、必要な場合は、このように一番外から囲むべし)

setSpinner() は、Redux の action と呼ばれる関数。この例での実態は:

export const showSpinner = (value: boolean, text: string = ""): any => {
    if (value) {
        if (++spinCount === 1) {
            return {
                type: "SPINNER",
                spinner: true,
                text,
            };
        }
    } else {
        if (0 < spinCount) {
            spinCount--;
        }
        if (spinCount === 0) {
            return {
                type: "SPINNER",
                spinner: false,
                text: "",
            };
        }
    }
};
let spinCount = 0;

---
connect() で、mapDispatchToPropsを登録しているので、Redux が本モジュールの props にsetSpinner関数を inject してくれている。その利用は至って簡単で、

        if (this.props.setSpinner) {
            this.props.setSpinner(true, "I am tired");
        }

等とすればよい(if は、Unit Test対策)。さすると、Reduxは、そのリスナー・モジュール達の props に変化を与えてくれる。spinner の UI を司るモジュールは以下:

import * as React from "react";
import { connect } from "react-redux";

interface Props {
    spinner: boolean;
    spinnerText: string;
}

interface State {
}

class Spinner extends React.PureComponent<Props, State> {
    public render() {
        return this.props.spinner ? (
            <div className="lpmodal">
                <span className="text">{this.props.spinnerText}</span>
            </div>
        ) : null;
    }
}

const mapStateToProps = (state: any) => ({
    spinner: state.spinner,
    spinnerText: state.spinnerText,
});

export default connect(mapStateToProps, () => ({}))(Spinner);

即ち、Reduxの state の spinner, spinnerText の変化を対象に subscribe している。

---
イベントの流れ、纏め:
  • あるモジュールにて、AjaxでAPI からデータを持ってくる直前に spinner を表示したいよ、というアクションを Redux に送る。
  • この例では、アクション関数にて、複数のほぼ同時 Ajax calls (かなりあり得る)をすっきりさせるため、間引きを行っている。
  • Reduxは、アクション関数からアクションが返ってくると、reducer を呼んでくるので、シンプルに新 state を返す。(間引きなどの処理は、reducer ではなく action で実施すべきらしいのでそうしている、確かにその方がよりサイレントな感じで良いと思う)。
  • その結果、Redux はリスナーの props をくすぐってくれる。
  • props がくすぐられた結果、spinner が表示されたり、消去されたりするのは React の真骨頂であり、簡単。

コメント

このブログの人気の投稿

HiddenFor 要注意

SPA を IIS から流す際の ASP 側のルーティング

Jest の テスト・スクリプトをデバッグする術