Redux超入門

Rexuxのチュートリアル的な記事です。初心者が数日触ったことを自分で自分に解説しているような感じなので、間違ったことを書いているかもしれません。あしからず。

前提知識として以下を要します。

  • 基本的なReact
  • 各種ES6の構文
  • npmによるパッケージ操作
  • モジュール

下準備

解説を簡単にするため、create-react-appでアプリケーションの雛形を作成し、そこから開始します。

create-react-app my-redux
cd my-redux
npm install --save-dev redux

srcディレクトリをindex.jsを残し全て削除しておいてください。

Rexuxとは何か

ステートフルなReactはコンポーネント自身が状態を管理します。簡易なアプリケーションの場合はシンプルで良いのですが、それなりの規模になるとコンポーネントの状態を1つ1つ管理するのが面倒になってきます。そこでアプリケーション全体で状態を管理する場所を作ろうというのが、Reduxです。

あらかじめ書いておくと、Reduxはすべてのアプリケーションで必ずしも役立つ機能ではないということです。小規模なアプリケーションの場合は、複雑になるだけであまりメリットは得られないでしょう。

Reduxの大まかな流れ

Reduxは大きく分けてストアアクションレデューサーで成り立っています。

Reduxを利用すると、すべての状態はストアと呼ばれるコンテナに保存されます。ストアはすべてのオブジェクトの状態が保存される状態ツリーです。どのUIコンポーネントもストアを介し状態にアクセスできるのですが、状態を変更する場合は、アクションをディスパッチする必要があります。ディスパッチとは、実行可能な情報をストアに送ることを意味します。

そして、ストアはアクションを受け取ると、それを関連するレデューサーに移譲します。レデューサーはアクションを実行した後、新しい状態を返す単なる関数に過ぎません。

Reduxはデータを一方向にのみ流すことができます。

ストアとは

ReduxストアはReduxで稼働するすべてのパーツをオーケストレーションします。ストアはアプリケーション全体の状態が格納されます。このため、Reduxを使うにはストアを作成する必要があります。

以下を作成してください。

// src/store.js
import { createStore } from 'redux';
import rootReducer from './reducers';

// ストアを作成
let store = createStore(rootReducer);

export default store;

createStore()はストアを作成する関数です。引数はレデューサーを指定します。レデューサーが何かは後述しますが、ストアはレデューサーを引数にしてのみ構築できます

レデューサーとは

レデューサーは単なる関数です。レデューサーは現在の状態とアクション(後述)の2つの引数を取り、次の「状態」を返すのが決まり事となっています。

/*
* レデューサー
* @param state 現在の状態オブジェクト
* @param action typeとpayloadを持つオブジェクト
* - type:アクションタイプ
* - payload:状態を更新されるためのデータ
* @return state 次の状態オブジェクト
*/
const reducer = (state, action) {
    return state;
}

reducersディレクトリを作成し、レデューサーを作成しましょう。

// src/reducers/cart-reducer.js

// アクション名の(定数)をインポート
import { ADD_TO_CART } from '../actions/cart-actions';

const initialState = {
    cart: [
        {
            product: 'coke',
            quanity: 1,
            cost: 130
        },
        {
            product: 'milk',
            quanity: 3,
            cost: 300
        }
    ]
};

// レデューサー関数
export default function (state = initialState, action) {
    switch (action.type) {
        case ADD_TO_CART: 
            {
                return {
                    ...state,
                    cart: [...state.cart, action.payload]
                }
            }
        default:
            return state;
    }
}

レデューサー内はアクションのタイプによりswitchで分岐させるのが一般的です。ADD_TO_CART型のアクションがアプリケーションのいずれかでディスパッチされると、ここで定義したコードが呼び出されます。

アクションの型はaction.type、状態更新のデータはaction.payloadに格納されています。ここでは、展開演算子を利用して、状態オブジェクト内のcartに新しい内容を追加しています。

Immutability

Reduxにはいくつかの原則があり、その中の1つにImmutabilityというものがあります。やや語弊もありつつ簡単に述べると、その場で内容を直接変更してはいけないということです。レデューサーは、以前の状態を受け取り新しい状態を返しますが、直接この状態を編集してはいけません。レデューサーは与えら得れた入力に対して、まったく同じ出力を返す純粋な関数である必要があります。

例えば上記では展開演算子を用いていますが、これがpush()を利用した場合、Immutabilityを破ることになります。pushは直接元の要素を追加するためです。

自分もよくわかっていない部分もありますが、基本的に上記のようなケースの場合、追加は展開演算子、更新はmap()、削除の場合はfilter()を使えば問題はないようです。

アクション

Reduxにおける状態を変更する唯一の方法は、ストアにアクションをディスパッチすることです。アクションはメソッドのようなものをイメージするかと思いますが、実際は以下のようなJavascriptのオブジェクトです。

{
    type: 'ADD_TO_CART',
    payload: {
        product: 'coke',
        quanity: 1,
        cost: 130
    }
}

アクションは状態の変更方法を記述するtypeと、payloadという状態を変更するデータを持ちます。通常、アクションは複数存在し、関数でラップするのが一般的です。こうした関数をアクションクリエイターと呼びます。

それではアクションクリエイターを配置しましょう。src/actionsディレクトリを作成する必要があります。

// src/actions/cart-actions.js
export const ADD_TO_CART = 'ADD_TO_CART';

export function addToCart (product, quanity, cost) {
    return {
        type: ADD_TO_CART,
        payload: {
            product, quanity, cost 
        }
    };
}

addToCart()はアクションをディスパッチする際に利用します。これは後ほど説明します。

レデューサーをまとめる

現在はcartReducerという1つのレデューサーのみが定義しています。実際の開発ではより多くのレデューサーが定義されることになるでしょう。その場合、一元管理しているはずの状態の管理や初期化が面倒になってきます。

ReduxにはcombineReducersというヘルパー関数が用意されています。これは値が異なる複数のレデューサーを呼び出し、1つのレデューサーにまとめて(変換して)くれます。

以下のようなファイルを作成しましょう。

//src/reducers/index.js
import { combineReducers } from "redux";
import productsReducer from "./products-reducer";
import cartReducer from "./cart-reducer";

const allReducers = {
    shoppingCart: cartReducer,
    products: productsReducer
};

const rootReducer = combineReducers(allReducers);

export default rootReducer;

新しくproductsReducerというレデューサーを作成してインポートしていますが、今回は解説のために定義しただけで利用はしません。何もしないレデューサーですが、ファイルだけは作成しておきましょう。

// src/reducers/products-reducer.js
export default function (state = [], action) {
    return state;
}

1番最初にストアの解説をしたセクションを見直してみましょう。

let store = createStore(rootReducer);

createStore()はレデューサーを指定することで、ストアを作成することができるのでした。今回は先ほど解説したcombineReducers()を使い2つのレデューサーを1つのレデューサーにまとめたものを指定しています。

2つのレデューサーは定義時にキーを設けていますが、これは以下のようにReduxストアの状態を操作・確認するときに便利です。キー名がついているので、異なるレデューサーを介して作成された内容であっても、同一のオブジェクト内で管理・把握できます。

例えば、以下はshoppingCartキー(cartReducer)を使いストアに追加されたのはcartが2件、products(productReducer)は0件です。

ちなみに、combineReducersを使わなくても単一のレデューサーしかないのならば、createStore(cartReducer)とするだけで十分です。

状態の確認

以下のファイルを作成し、Reduxストアの状態を確認してみましょう。

// src/index.js
import store from "./store";
import { addToCart } from "./actions/cart-actions";

console.log('初期値:', store.getState());
store.dispatch(addToCart('coffee', 1, 200));
console.log("追加後:", store.getState());

実行すると、以下のような状態が確認できるでしょう。

getState()で現在のストアの状態を確認できます。そして要素を追加しているのが以下です。

store.dispatch(addToCart('coffee', 1, 200));

ストアの状態を更新するのは、アクションをディスパッチする必要があると説明しました。それがここです。ディスパッチはdispatch()で行います。

// アクションクリエイター内(抜粋)
export function addToCart (product, quanity, cost) {
    return {
        type: ADD_TO_CART,
        payload: {
            product, quanity, cost 
        }
    };
}

アクションがディスパッチされると処理はレデューサーに移譲するのでした。また、呼ばれるレデューサーはアクションのタイプで決まります。つまり、今回だとADD_TO_CART型のアクションです。switch内では、新しい状態としてaction.payloadを介して受け取った値を追加して返しています。これによりReduxストアは更新されます。

export default function (state = initialState, action) {
    switch (action.type) {
        case ADD_TO_CART: 
            {
                return {
                    ...state,
                    cart: [...state.cart, action.payload]
                }
            }
        default:
            return state;
    }
}

基本の流れはこんな感じです。ただ、現状は単にReduxストアを管理する部分を作っただけに過ぎません。実際はReactと連携させる必要があります。次回、ReactとReduxの連携方法について軽く触れます。