Redux の基本 - React との連携 -

 みなさんこんにちは!DeNA Games Osaka 技術編成部のさいです。
前回は、Redux を使って、ToDo アプリのデータのフローについて実装しました。今回はその Redux の実装を使って、React との連携を行っていきたいと思います。

React とは

 React は Facebook が開発している View 用の JavaScript ライブラリです。大きな特徴として仮想DOMと呼ばれるレンダリング機構がそなわっており、完成品のあるべきDOMの姿を記述することで、変更があった差分だけ DOM を追加/変更/削除してくれます。

 仮想DOMはただの JavaScript の関数なので、React をそのまま利用しようとすると、DOM の記述を JavaScript の関数として書かないといけませんが、JSX を利用することで、非常にHTML に近しい形で DOM を記述することができます。

環境構築

 前回の記事では、下記のコマンドを実行して、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

 今回は、前回のコマンドに加えて下記コマンドの実行をし、React 及びReact との連携に必要なライブラリ及びツールを揃えます。

# 今回新規に実行するコマンド
npm install --save-dev babel-preset-react
npm install --save react react-dom react-redux
echo '{"presets": ["react", "es2015"]}' > .babelrc # es2015 に加えて、react の preset を追加

それでは実装に入っていきましょう。

実装

 前回、下記のように、Redux を使用して、ToDo リストを console に出力する
コードを書いたかと思います。


/* 前回のおさらい */
'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));

 こちらのコードに React のコードを追加して、
ToDo アプリケーションを作っていきたいと思います。

 それでは ToDo アプリを表示するため、dist ディレクトリに index.html を追加します。


<!-- dist/index.html -->
<html>
    <head>
        <title>ToDoアプリ</title>
    </head>
    <body>
        <div id="container"></div>
        <script src="./app.js"></script>
    </body>
</html>

 script タグで同 dist ディレクトリ内の app.js ファイルを読み込んでいます。
また、React のDOMの描画先として、div タグを作成しておきます。

 それでは、React のコンポーネントを作成していきますが、その前に、前回作成した Redux 周りのコードのうち、console.log 出力している箇所は不要なので削除しておきます。


/* 削除 */
/*
// 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));
*/

それではReact のコンポーネントを作成します。


import React from 'react';

const App = () => (
    <div>
        <AddToDoComponent />
    </div>
)

 App コンポーネントを作成しました。これが ToDo アプリケーションを表すコンポーネントとなります。コンポーネントの中では、div タグで囲んだ AddToDoComponent を呼んでいます。js ファイル中に突然、HTMLタグが登場しましたが、このHTMLタグ部分が jsx となります。この jsx 部分は、最初の環境構築時にインストールした babel-preset-react によって、ビルド時に下記のようなコードに変換されます。


var App = function App() {
    return React.createElement(
        'div',
        null,
        React.createElement(AddToDoComponent, null)
    );
};

 上記のコードを見てわかる通り、jsx 部分は、React の createElement を呼ぶただのJavaScript のコードであり、実体は createElement で生成された仮想DOMというわけです。

 App コンポーネントの中に記述した AddToDoComponent についてはまだ未実装でした。それでは、ToDo を追加するフォームである AddToDoComponent を実装します。


import React from 'react';
import { Provider,connect } from 'react-redux'; // 追加

// 追加
let AddToDoComponent = ({dispatch}) => {
    let input;

    const onSubmit = (e) => {
        e.preventDefault();
        dispatch(addToDo(input.value));
        input.value = "";
    };

    return(
        <div>
            <form onSubmit={onSubmit}>
                <input ref={ node => {input = node} }/>
                <button>Todo に追加する</button>
            </form>
        </div>
    );
};

AddToDoComponent = connect()(AddToDoComponent);

 App コンポーネントと同様に、jsx で、AddToDoComponent を作成し、
react-redux を使って、Redux と連携を行っています。上から解説します。


import { Provider,connect } from 'react-redux'; // 追加

 react-redux から connect 関数と Provider コンポーネントをインポートしています。connect は Redux と React コンポーネントの接続を行う関数です。Provider はまた後ほど使用します。


let AddToDoComponent = ({dispatch}) => {
    let input;

    const onSubmit = (e) => {
        e.preventDefault();
        dispatch(addToDo(input.value));
        input.value = "";
    };

 AddToDoComponent を定義しています。引数で dispatch を受け取っています。後ほど説明しますが、AddToDoComponent は connect 関数でラップされて使用されます。connect 関数でラップされたコンポーネントは、引数として dispatch 関数を受け取ります。これは Store に紐付けられた Redux の dispatch 関数です。dispatch 関数は、onClick などの View 側の操作により、Store に対して Action を投げたい場合に使用します。

 AddToDoComponent 内で、ToDo に追加するボタンが押下された時に実行される onSubmit 関数を定義します。onSubmit 関数の内部では、先ほど説明しました dispatch 関数を通じて、前回の Redux コードを作成した際に定義した addToDo Action を Store へ dispatch しています。


    return(
        <div>
            <form onSubmit={onSubmit}>
                <input ref={ node => {input = node} }/>
                <button>Todo に追加する</button>
            </form>
        </div>
    );

最後の return 文にて、ToDo を追加するための、フォームの仮想DOMを作成します。


AddToDoComponent = connect()(AddToDoComponent);

 作成した AddToDoComponent を、react-redux の connect 関数でラップしています。これで、AddToDoComponent は Redux と連携することができます。

 これで、AddToDoComponent 及び App コンポーネントが作成できました。
これを HTML上にレンダリングします。


import { render } from 'react-dom'; // 追加

render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('container')
)

 先ほど react-redux でインポートした Provider コンポーネントで、App コンポーネントをラップします。Provider の store プロパティに、前回作成した Redux の store を渡すことで、App 及びその中の AddToDoComponent が store にアクセスすることができるようになります。

 そして、react-dom の render 関数によって、React コンポーネントを div タグの中に描画します。

 ここまでで、ビルドすることで、タスクを追加するためのフォームが表示されたかと思います。


# ビルドコマンド(前回記事の再掲)
browserify ./src/app.js -t babelify -o ./dist/app.js  
キャプチャ02.PNG

 

 

 ToDo へ追加するボタンを押下することで、onSubmit 関数を通じて、dispatch が実行され、Store に新しい ToDo が追加されますが、またToDo 一覧を表示していないので、
何も起こりません。

 引き続き、ToDo 一覧である、ToDoListComponent を作成していきます。


let ToDoListComponent = ({todo}) => {
    return (
        <ul>{
            todo.map( (t) => {
                return <li key={t.id}>{ t.text }</li>;
            })
        }</ul>
    );
};

function mapStateToProps(state) {
    return {
        todo: state.todo
    };
}


ToDoListComponent = connect(mapStateToProps)(ToDoListComponent);

 先ほどの AddToDoComponent と同様にコンポーネントを作成したあとに、connect 関数を通じて、Redux と連携するコンポーネントを作成しています。

 先ほどの AddToDoComponent と違い、ToDoListComponent では、mapStateToProps 関数を定義して、connect 関数の第一引数に指定しています。

 mapStateToProps 関数の state には、Store が更新されるたびに、Store の情報が渡ってきます。それを、React のプロパティとして受け取れる形に変換するのが、mapStateToProps の役割です。

 最後に ToDoListComponent を App コンポーネントから呼びます。


const App = () => (
    <div>
        <AddToDoComponent />
        <ToDoListComponent />
    </div>
)

ビルドすると、タスクの一覧が表示されました。

 

 

 

 

 それでは仕上げとして、このToDo を完了/未完了にできるようにしましょう。ToDoListComponent を修正します。


// 引数にて dispatch を受け取る
let ToDoListComponent = ({todo, dispatch}) => {
    return (
        <ul>{
            todo.map( (t) => {
                return <li key={t.id}>
                    {/* span タグと、checkbox を追加 */}
                    <span style={ { textDecoration: t.completed ? 'line-through' : 'none' } }>
                        { t.text }
                        <input type="checkbox" onClick={ (e) => {  dispatch(toggleToDo(t.id))  } } checked={ t.completed } />
                    </span>
                </li>;
            })
        }</ul>
    );
};

 チェックボックスの ON / OFF によって、タスクの状態を変更するため、AddToDoComponent と同様に dispatch 関数を受け取っています。

 チェックボックスがクリックされたら、前回の Redux コードを作成した際に定義した toggleToDo Action を dispatch を通じて Store に投げています。Store の状態が変更されると、react-redux が変更を検知し、React コンポーネントの再描画が走ります。

 これで ToDo が完成しました。

 

 

 

 

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


'use strict';
import { createStore } from "redux";
import React from 'react';
import { render } from 'react-dom';
import { Provider,connect } from 'react-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: []});

let AddToDoComponent = ({dispatch}) => {
    let input;

    const onSubmit = (e) => {
        e.preventDefault();
        dispatch(addToDo(input.value));
        input.value = "";
    };

    return(
        <div>
            <form onSubmit={onSubmit}>
                <input ref={ node => {input = node} }/>
                <button>Todo に追加する</button>
            </form>
        </div>
    );
};

AddToDoComponent = connect()(AddToDoComponent);


let ToDoListComponent = ({todo, dispatch}) => {
    return (
        <ul>{
            todo.map( (t) => {
                return <li key={t.id}>
                    <span style={ { textDecoration: t.completed ? 'line-through' : 'none' } }>
                        { t.text }
                        {/* 追加 */}
                        <input type="checkbox" onClick={ (e) => {  dispatch(toggleToDo(t.id))  } } checked={ t.completed } />
                    </span>
                </li>;
            })
        }</ul>
    );
};

function mapStateToProps(state) {
    return {
        todo: state.todo
    };
}


ToDoListComponent = connect(mapStateToProps)(ToDoListComponent);

const App = () => (
    <div>
        <AddToDoComponent />
        <ToDoListComponent />
    </div>
)

render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('container')
);

まとめ

 ちょっと駆け足になってしまいましたが、Redux と React についていかがだったでしょうか。
大規模なアプリケーション開発において、アプリケーションの「状態」をどのように管理するかについて、Redux では、Action, Dispatcher, Store, View という4つの概念を用いて、情報のフローを明確にするアプローチを取っています。

 このような簡単なToDo アプリケーションだと、最初に書くコードの記述量が増えてしまうのですが、コードの規模が大きくなればなるほど、コードの見通しがよくなり、のちのちの機能修正/改修の際の保守性が向上します。

 ぜひ皆さんも、Redux と React を試してみてください。

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