JavaScript
redux
esnext
redux-actions

redux-actionsを使って、reduxの記述で楽をしよう!

今日はredux-actions(記載時点ではv2.0.1)という公式推奨パッケージを使って、
reduxのactionとreducerを楽にスッキリ書く方法を紹介します。

追記:現在は公式のドキュメントができているので、こちらを参照するとより確実です。
https://redux-actions.js.org/docs/api/index.html

アクションの形式

reducerを書く際に下記の形式(※1)を知っている必要があります。

{
    type    : {symbol|string}, // アクションタイプ
    payload : {any},           // メインの情報
    error   : {boolean},       // エラーかどうか
    meta    : {any},           // payloadに乗らなかった情報
}

アクション(クリエイター)の生成

まとめて楽チンcreateActionsを使います。
キー名にアクションタイプを指定し、アクションはそれのlowerCamelで作られます。

import { createActions } from 'redux-actions';
import types from './actionTypes';

export default createActions(
    // payloadを整形したい場合、またはmetaを利用したい場合は
    // 第一引数にオブジェクトでその定義をする。
    {
        // payloadだけを使う場合
        // アクションの引数を元に、どんな形でpayloadを作るか定義します。
        // この形式の場合、metaは作られません。
        [types.GET_HOGE] : (...args) => { test1 : args[0], test2 : args[1] },

        // metaも使う場合
        // 値に配列を指定し、1つ目がpayload, 2つめがmetaの定義です。
        [types.GET_FUGA] : [
            (...args) => args[0],
            (...args) => args[1],
        ],
    },
    // その他シンプルな場合
    // payloadはアクション実行時の第一引数をそのままわたし、
    // metaは使わない時は、
    // 第二引数以降にタイプだけ渡していきます。
    types.GET_PIYO,
    types.GET_PUNI,
);

同じアクションを手書きすると下記になります。だいぶ楽できてますね!

// before =================================================
export default {
    getHoge : (arg0, arg1) => {
        type    : types.GET_HOGE,
        payload : {
            test1 : arg0,
            test2 : arg1,
        },
    },
    getFuga : (arg0, arg1) => {
        type    : types.GET_FUGA,
        payload : arg0,
        meta    : arg1,
    },
    getPiyo : (arg) => {
        type    : types.GET_PIYO,
        payload : arg,
    }, 
    getPuni : (arg) => {
        type    : types.GET_PUNI,
        payload : arg,
    },
};
// after =====================================================
export default createActions(
    {
        [types.GET_HOGE] : (...args) => { test1 : args[0], test2 : args[1] },
        [types.GET_FUGA] : [
            (...args) => args[0],
            (...args) => args[1],
        ],
    },
    types.GET_PIYO,
    types.GET_PUNI,
);

もしアクションタイプ名とアクション名を変えたい場合は、createActionで個別に作ると変えられます。
第一引数がアクションタイプ、第二がpayloadの設定関数、第三がmetaの設定関数になります。

// createActionsだと "getHoge" となるところを "get" にしたい時
export default {
    get : createAction(
        types.GET_HOGE,
        (...args) => { test1 : args[0], test2 : args[1] },
    ),
};

reducerの生成

handleActionsでこちらも楽チン生成します。

import actions from './action';

const initialState = {
    test1 : '',
    test2 : '',
    test3 : '',
    piyo  : '',
    puni  : '',
};

// 第一引数にはreducerの設定を入れたオブジェクトを、
// 第二引数には初期stateオブジェクトを渡します。
export default handleActions({
    // キーについて
    //  switchで書く場合のアクションタイプをオブジェクトのキーとしますが、
    //  ここではcreateActionsで作られたアクション自体を [] で囲んでいます。
    //  オブジェクトのキーに[]で囲んだ変数を入れると、文字列と判定され、
    //  toString() が呼ばれます。
    //  createActionsで作られたアクションはtoStringでアクションタイプを返すので、
    //  このように「このアクションが発行されたら」と、直感的な書き方にすることができます。
    // バリューについて
    //  バリューには関数を作成します。
    //  引数は通常のreducerと同様、現状のstateと、actionが渡されます。
    //  actionは ※1 の形式で渡されます。
    [actions.getHoge] : (state, action) => ({
        ...state,
        test1 : action.payload.test1,
        test2 : action.payload.test2,
    }),
    [actions.getFuga] : (state, actions) => ({
        ...state,
        test3 : (actions.meta === 'FUGA') ? action.payload : '',
    }),
    [actions.getPiyo] : (state, action) => ({
        ...state,
        piyo : action.payload,
    }),
    [actions.getPuni] : (state, action) => ({
        ...state,
        puni : action.payload,
    }),
}, initialState);

同じreducerをswitchで書く下記のようになります。
幾分かスッキリした印象です。
immutable.jsを使うとよりスッキリしますが、話がそれるのでここでは割愛します。
また、actionTypeをreducer側で呼ばないことで、actionCreator側だけで使うことになり、
影響範囲を狭めることができています。

// before ==================================================
import types from './actionTypes';

... initialState省略 ...

export default (state = initialState, action) {
    switch(action.type) {
        case types.GET_HOGE:
            return Object.assing({}, state, {
                test1 : action.payload.test1,
                test2 : action.payload.test2,
            });
        case types.GET_FUGA:
            return Object.assign({}, state, {
                test3 : (actions.meta === 'FUGA') ? action.payload : '',
            });
        case types.GET_PIYO:
            return Object.assing({}, state, {
                piyo : action.payload,
            });
        case types.GET_PUNI:
            return Object.assing({}, state, {
                puni : action.payload,
            });
        default:
            return state;
    }
}
// after ===================================================
import actions from './action';

... initialState省略 ...

export default handleActions({
    [actions.getHoge] : (state, action) => ({
        ...state,
        test1 : action.payload.test1,
        test2 : action.payload.test2,
    }),
    [actions.getFuga] : (state, actions) => ({
        ...state,
        test3 : (actions.meta === 'FUGA') ? action.payload : '',
    }),
    [actions.getPiyo] : (state, action) => ({
        ...state,
        piyo : action.payload,
    }),
    [actions.getPuni] : (state, action) => ({
        ...state,
        puni : action.payload,
    }),
}, initialState);

余談ですが、こちらのhandleActionsを知らない頃に、自前でreducerをすっきりさせる記事を書きました。
内部的には似た形なので、よければこちらもご覧ください。
Reduxのreducerはオブジェクトで書こーず!!

その他の機能

公式では combineActions と、ミドルウェアと組み合わせる場合の説明があります。
気になる方は読んでみてください。
npm redux-actions

まとめ

  • redux-actions の機能を使って楽をする方法を紹介
    • 特に createActions, handleActions の使い方と、利用前後の差分を紹介
  • メリット
    • 楽をできる、記述がスッキリする
  • デメリット
    • 追加の仕組みを知る必要がある、元々のreduxの動きを把握できてない人にはわかりにくくなる