reduxはredux-actionsギプスをつけて養成

reactアプリケーション開発においてコンポーネントに引数として渡されるpropsと、スコープの大きな実行時変数としてのcontextに加えて、もっぱらコンポーネント内部の状態管理としてのstateをあれこれ操作するのがデータフローの基本だったのですが、ここ一年ぐらいで様子が一気に変わってしまいました。reactの普及と対象とするアプリケーション規模が大きくなることによる複雑性の課題に応えてFluxが提唱され、その実装バリエーションで淘汰を経たreduxの登場です。それまで標準であったコンポーネントstate管理によって多数散在してしまった複数ステートマシンの連携を否定して、アプリケーションレベルで一つの大きなステートマシンをつくってpropsを通じてのみコンポーネントツリーに反映させるという。たくさんあるものをn:nに対応させていくってのは複雑を、常に抽象から具体へ1:nになる流れに修正すると一気にシンプルになり、わかりやすくなりました。

redux-actions

そんなreduxはこれまたライブラリとしてみると極薄で、実装の際に自由度がとても高い。これはテストや設計などでメリットとなる一方で、どう書いてもいいってのは、そもそもに開発者に悩みを与え、構成としては将来に技術的負債を生じさせてしまうリスクを持ってます。

GitHub - acdlite/redux-actions: Flux Standard Action utilities for Redux.

redux-actionsはその説明の冒頭にあるように、FSA(https://github.com/acdlite/flux-standard-action, =Flux Standard Action)として引いたガイドラインのライブラリです。reduxでマジにどうやったっていいactionCreatorの仕様と実装に堅めのガイドラインをFSAが引き、redux-actionsが実装の手助けをしてくれる。。。って今きがついたのですが、作者いっしょじゃん。なるほどねー。FSAの冒頭にあるように「A human-friendly standard for Flux action objects. 」なのはそのとおり。すでにreduxエコシステム界もこのFSAをスタイル標準として反応している様子なので、逆らわずに巻かれていいとおもう。

// /actions/index.js
import { createActions } from 'redux-actions';

export default createActions(
    'REQUEST_LIST',    // -> actionCreatorの識別子はrequestListと自動的に名付けられる
    'RECEIVE_LIST',      // -> action.typeは'RECEIVE_LIST'
);

ActionCreatorはこれでOK。それぞれの名前に応じて、引数をそのままaction.payloadに積むactionCreatorが用意されます。ドキュメントにも書かれてるけど、createActionsではなくて本当はcreateActionCreatorsとでもするべきなんでしょう。でもcreateActionsです。プレーンオブジェクトに用意したactionCreatorを詰め込んだものを返してくれますので、そのままexport defaultしました。

対するreducerもまっさらにswitch文を書く代わりにredux-actionsの支援を用いて書けます。

// /reducers/AppReducer.js
import { handleActions } from 'redux-actions';
import actions from '../actions';

export default handleActions({
    [actions.requestList]: state => ({
        ...state,
        fetch: true,
    }),
    [actions.receiveList]: {
        // 正常時
        next: (state, { payload }) => ({
            ...state,
            list: payload,
            errorMessage: null,
            fetch: false,
        }),
        // 例外時
        throw: (state, { payload }) => ({
            ...state,
            list: null,
            errorMessage: payload.message,
            fetch: false,
        }),
    },
}, { /* default state here */ });

こちらは間違いなくhandleActionsで名も実もいいですね。import actions from '../actions';から [actions.requestList]:としてるところは、createActionsで用意されたActionCreatorがtoString()でAction Typeを返してくれる実装をしてるからです。気がきいてます。handleActionsの馬鹿でかくなってしまう第一引数に続いて、第二引数に空オブジェクト(default state hereのところ)をおいてますが、これはstateの初期値設定です。このオブジェクトを省略するとヌルポが起きるので注意。

外のAPIを叩いてもどってくる作りのときには、FSAがActionCreatorに渡された引数を常にpayloadに積むことと、例外通知にはpayloadにエラーを突っ込んだ上で、action.errorをtrueにするということが効いてきます。reducerの実装が関数のときは正常時も例外時も関係なくそのまま実行するし、nextとthrowという名前のメソッドを持つオブジェクトを設定すれば、上の例のreceiveListがそうですが、正常時はnextを実行し例外時にはthrowを実行しわけるreducerを作れる。小さな規約でシンプルかつ堅い。

reduxでmiddleware書くなら、FSA前提でつくっちゃったほうが悩みが少ないです。たとえば以下のCookieと値をコピーするMiddlewareでは、action.metaというもう一つの規約をもちいて処理のきっかけをもたせた上で、値は常にaction.payloadに乗ってるという前提あっての実装です。そうじゃないとどうくるかわからないactionに対応するためにもっと複雑な作りになっちゃう。

import Cookies from 'browser-cookies';

export default function (options = { expires: 1 }) {
    return (/* store */) => next => (action) => {
        const name = action.meta && action.meta.cookie;
        if (name && !action.error) {
            if (action.payload) {
                Cookies.set(name, JSON.stringify(action.payload), options);
            } else {
                const value = Cookies.get(name);
                if (value) {
                    try {
                        next({ ...action, payload: JSON.parse(value) });
                    } catch (e) {
                        next({ ...action, payload: e, error: true });
                    }
                    return;
                }
            }
        }
        next(action);
    };
}

babel

ところで、ESの様々な新文法つかうとreduxって気持ちいいんですが、トランスパイルするためにはbabelで設定が必要。

  "babel": {
    "plugins": [
      "transform-decorators-legacy"
    ],
    "presets": [
      "es2015",
      "react",
      "stage-1"
    ]
  },

私はpackage.jsonでこのようなbabelの設定しています。presetsのstage-1がないと、reducerでstateを「...」を用いて展開するのができない。これができるとできないではコードに大きな違いが生じます。transform-decorators-legacyは今回とは別で、redux-formの私の書き癖のために登録しています。

ちなみにESLintは以下のように。WebStormの標準の体裁にあわせたほかは、airbnbです。去年書いてた時にはairbnbはどうにも馴染まないルールが多くて無理ゲーだったんだけど、いま書いてみると特に違和感ない。airbnbが変わったのか、私が変わったのか、どちらかは不明です。airbnbはreactでstateを直接設定するのを嫌っており、reduxが標準採用されてる予感です。

  "eslintConfig": {
    "extends": "airbnb",
    "env": {
      "browser": true,
      "jasmine": true
    },
    "parser": "babel-eslint",
    "rules": {
      "indent": [
        "error",
        4,
        {
          "SwitchCase": 1
        }
      ],
      "react/jsx-indent": [
        "error",
        4
      ],
      "react/jsx-indent-props": [
        "error",
        4
      ],
      "react/jsx-filename-extension": "off",
      "max-len": [
        "error",
        120
      ]
    }
  },

react/jsx-filename-extensionはオフにしないとimport文がうざいことになる。なんでこれオンになってるんだろう?遠回しにJSXを直接書くなってことなのかな?