Redux の基本

 みなさんこんにちは!DeNA Games Osaka 技術編成部のさいです。

前回前々回で、JavaScript フレームワーク Mithril.js と Riot.js について触れてきました。

今回は、同じ JavaScript フレームワークである React に触れていきたいと思いますが、その前に、 React と一緒によく語られる Flux アーキテクチャ、及び Redux についてご説明させてください。
 

Redux とは

 React は JavaScript フレームワークの1つですが、View の機能のみしか提供していません(そういう意味ではフレームワークというよりライブラリに近しいです)。

React でアプリケーションを構築するに当たって、View 以外のアーキテクチャをどうするかについては、Flux というアーキテクチャが提案されておりました。

Flux はあくまでアーキテクチャであり、実装については様々なライブラリが乱立していましたが、ここ最近は Redux が主流になっているようです。

 JavaScriptのアプリケーションの複雑に伴い、アプリケーションの state(状態)をどう管理するかが、これまでの課題でしたが、Flux アーキテクチャでは下記のようにデータフローを一方向にすることで、状態の遷移をわかりやすくしています。
 

Action

 UIのクリックやHTTPリクエストが届いた等のメッセージ。各イベントがどのような操作を行うのかを決定し、Dispatcher を通して Store に伝える。
 

Dispatcher

 Action を Store に配送するハブ。
 

Store

 状態を保持する。Dispatcher から Action を受け取り、状態を更新する。
 

View

 Store を監視し、状態の更新に合わせて自身を再描画する。
 


Redux は Flux アーキテクチャをさらに押し進めたものであり、以下の三原則の制約を加えることで、さらに状態の管理や変更をわかりやすくしています。
 

1. Single source of truth(状態の源は1つだけ)

 Flux アーキテクチャにおいては、複数の状態(Store) を持つことも可能としていましたが、Redux においては Store は一つだけです。

2. State is read-only(状態は読み取り専用)

 View 等が直接状態を変更することはありません。状態を変更する方法は、変更内容を持った Action オブジェクトを発行する方法のみです。

3. Mutations are written as pure functions(変更はすべて純粋な関数で書かれる)

 Redux では Reducer が、Actionを元にどのように Store の状態を変更するかを定義します。この Reducer は、Store の状態を直接変更せず、新しい State オブジェクトを作って返します。

 Redux は React 専用のフレームワークというわけではありません。まずは Redux だけで触ってみて、Flux アーキテクチャにおけるデータのフローについて見ていきたいと思います。

 

ToDoアプリを作ってみる

Mithril.js や Riot.js と同じように、ToDo アプリを作っていきたいと思います。ただ、今回は React との連携までは行かずに、Redux のみで、コンソールに出力を吐くアプリを作っていきたいと思います。

まずは下記コマンドで、Redux および ES2015 の環境を揃えます。

mkdir src dist
npm init --yes
npm install -g browserify
npm install --save-dev babelify@7.2.0 babel-preset-es2015
npm install --save redux
echo '{"presets": ["es2015"]}' > .babelrc


それでは、Store 及び Reducer を作ってみたいと思います。 src/app.js を作って、下記を書いてみましょう。
 

'use strict';
import { createStore } from "redux";

function reducer (state, action) {
    return state;
}

const store = createStore(reducer, {todo: []});

console.log(store.getState());


 Reducer を作成し、それを元に Store を作成しています。

Redux においては Dispatcher は存在せず、Reducer が Action を元に、Store の状態の変更を担います。createStore の第二引数は、state の初期値ですね。ToDoリストなので、各個別のToDo を入れるために todo プロパティに空配列を設定しておきます。
 

 それでは、下記コマンドにて ES2015 のコードを ES5 に変換して実行します。

browserify ./src/app.js -t babelify --outfile ./dist/app.js
node ./dist/app.js

 { todo: [] } というのがコンソールに表示されたかと思います。Store を定義し、その中身を取り出しただけなので、設定した初期値がそのまま表示されていますね。


 それでは、この Store に向けて投げる Action を作りたいと思います。Action は Store の状態を更新するために投げます。今回の ToDo アプリでは、ToDo リストへのToDo の追加および各 ToDo の完了/未完了の変更をできるようにしたいと思います。

// ToDo を一意に特定できる ID
let nextTodoId = 0;

// ToDo の追加
function addToDo (text) {
    return {
        type: 'ADD_TODO',
        id: nextTodoId++,
        text: text,
    };
}
// ToDo の完了/未完了
function toggleToDo (id) {
    return {
        type: 'TOGGLE_TODO',
        id: id,
    };
}


ToDo リストへのToDo の追加および各 ToDo の完了/未完了の変更を行うAction を定義しました。それでは、Reducer の中身を実装します。

function reducer (state, action) {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                todo: state.todo.concat({
                    id: action.id,
                    text: action.text,
                    completed: false
                })
            };
        case 'TOGGLE_TODO':
            return {
                todo: state.todo.map(t => {
                    if (t.id !== action.id) {
                        return t;
                    }

                    return Object.assign({}, t, {completed: !t.completed});
                })
            };
        default:
            return state;
    }
}

 Reducer は Action を元に、Store の状態を変更します。

先ほども説明させて頂きましたが、Redux の3つめの原則「変更はすべて純粋な関数で書かれる」の通り、Reducer は純粋な関数です。第一引数に、Store の現在の状態である state を受け取り、第二引数に、Store に向けて投げられた Action を受け取ります。state と action を元に、Reducer は新しい state を返します。

 Reducer 内で、引数で受け取った state を直接変更しないように気をつけています。

ADD_TODO Action では、state に対して直接 push して変更を加えず、concat によって新しい ToDo を追加した state を返しています。同様に TOGGLE_TODO Action では、Object.assign を使って、state 自身を壊さないようにタスクの完了/未完了を設定しています。


 それでは、ここまでで作成した Store, Reducer, Action を使って、ToDo リストに Action を投げてみたいと思います。下記のコードを記載します。

// Store に変更があれば、state を console に出力する
store.subscribe(() => console.log(store.getState()));

console.log("買い物を追加");
store.dispatch(addToDo("買い物に行く"));

console.log("銀行を追加");
store.dispatch(addToDo("銀行に行く"));

console.log("銀行に行くのをDone");
store.dispatch(toggleToDo(1));


 下記が出力されたかと思います。Store に対して、addToDo を dispatch すると、ToDoリストに、ToDoが一つ追加され、また、toggleToDo で ToDo の ID(今回はID: 1) を指定すると、ID: 1 の銀行に行くタスクが完了になりました。

買い物を追加
{ todo: [ { id: 0, text: '買い物に行く', completed: false } ] }
銀行を追加
{ todo:
   [ { id: 0, text: '買い物に行く', completed: false },
     { id: 1, text: '銀行に行く', completed: false } ] }
銀行に行くのをDone
{ todo:
   [ { id: 0, text: '買い物に行く', completed: false },
     { id: 1, text: '銀行に行く', completed: true } ] }


 最終的にコードは以下のようになりました。

'use strict';
import { createStore } from "redux";

// ToDo を一意に特定できる ID
let nextTodoId = 0;

// ToDo の追加
function addToDo (text) {
    return {
        type: 'ADD_TODO',
        id: nextTodoId++,
        text: text,
    };
}
// ToDo の完了/未完了
function toggleToDo (id) {
    return {
        type: 'TOGGLE_TODO',
        id: id,
    };
}

function reducer (state, action) {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                todo: state.todo.concat({
                    id: action.id,
                    text: action.text,
                    completed: false
                })
            };
        case 'TOGGLE_TODO':
            return {
                todo: state.todo.map(t => {
                    if (t.id !== action.id) {
                        return t;
                    }

                    return Object.assign({}, t, {completed: !t.completed});
                })
            };
        default:
            return state;
    }
}
const store = createStore(reducer, {todo: []});

// Store に変更があれば、state を console に出力する
store.subscribe(() => console.log(store.getState()));

console.log("買い物を追加");
store.dispatch(addToDo("買い物に行く"));

console.log("銀行を追加");
store.dispatch(addToDo("銀行に行く"));

console.log("銀行に行くのをDone");
store.dispatch(toggleToDo(1));


それでは次回は、ここまでの Redux の実装を使って、React との連携を行っていきたいと思います。

さいの入門シリーズはコチラ