このページの本文へ

逃げ出さないで!React+ReduxでいまどきSPA開発に触れてみる

2016年06月06日 16時23分更新

文●Dan Prince

  • この記事をはてなブックマークに追加
本文印刷
いまどきのSPA(Single Page Application)開発に欠かせない存在になった「React」。「自分とは関係なさそう」とまだ使っていないWeb制作者も、この記事でどんなものか体験してみては?

(この記事は、2016年5月3日に修正されました)

コンポーネントを使った単一方向のデータフローを採るReactのデータフローは、ユーザーインターフェースの構造を記述するのに理想的です。state(ステート)と連動するツール類が意図的に単純化されていることから、Reactが単に従来型のModel-View-Controllerアーキテクチャーの中のViewにすぎないように思うかもしれませんが、理想的なツールであることは確かです。

Reactだけで大規模なアプリを構築できないわけではありませんが、コードをシンプルにするためには、stateを別の場所で管理する必要があります。

Reactにはアプリケーションのstateを扱うための公式なソリューションはありませんが、Reactの考え方と特にうまく適応するライブラリーはいくつかあります。最近swは、次に紹介する2つのライブラリーとReactを組み合わせてプリケーションを構築している例が多くあります。

Redux

Reduxは、FluxElmのコンセプトをミックスして作られた、アプリケーションのstateのコンテナーとして動く小さなライブラリーの1つです。下記のガイドラインに沿っていれば、Reduxはどんな種類のアプリケーションのstateも管理できます。

  1. stateは常に1つのstoreに保管すること
  2. stateは変更せずにactionから起こること

Reduxのstoreのコアにあるのは、カレントアプリケーションのstateと1つのactionを取り出して組み合わせ、新しいアプリケーションのstateを作る関数です。これをreducerと呼びます。

Reactコンポーネントはactionをstoreに送り込む役目を果たします。受け取ったstoreは、再表示が必要なときにコンポーネントに通知します。

ImmutableJS

Reduxでは、アプリケーションのstateをmutateできないので、immutableデータ構造を使ったアプリケーションのstateのモデリングで実行します。

ImmutableJSは、変更可能なインターフェースを伴ったimmutableデータストラクチャーを提供しており、効率的に実装できます。ImmutableJSは、ClojureやScalaにインスパイアされて作られました。

デモ

実際にReactをReduxおよびImmutableJSと一緒に使って、完了済みと未完了のtodoの表示を切り替えられる程度の、シンプルなtodoリストを作ってみましょう。

コードはGitHubにあります。

セットアップ

プロジェクトフォルダを作り、npm initを使ってpackage.jsonファイルをイニシャライズしてから、あとで必要になる依存オブジェクトをインストールします。

npm install --save react react-dom redux react-redux immutable
npm install --save-dev webpack babel-loader babel-preset-es2015 babel-preset-react

次に、JSXES2015を使って書いたコードを、Babelでコンパイルします。Webpack上でのモジュールのバンドルリングプロセスの一環として実施します。

webpack.config.jsの中で、Webpackコンフィギュレーションを作成します。

module.exports = {
  entry: './src/app.js',
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: { presets: [ 'es2015', 'react' ] }
      }
    ]
  }
};

最後に、npmスクリプトを追加してpackage.jsonを拡張し、コードとソースマップをコンパイルします。

"script": {
  "build": "webpack --debug"
}

コードをコンパイルするたびに、npm run buildを実行する必要があります。

Reactとコンポーネント

コンポーネントを実装する前に、ダミーデータを作っておくとよいでしょう。コンポーネントを表示する際に、何が必要なのか感覚をつかみやすくなります。

const dummyTodos = [
  { id: 0, isDone: true,  text: 'make components' },
  { id: 1, isDone: false, text: 'design actions' },
  { id: 2, isDone: false, text: 'implement reducer' },
  { id: 3, isDone: false, text: 'connect components' }
];

アプリケーションで必要になるのは、<Todo/><TodoList />のReactコンポーネントだけです。

// src/components.js

import React from 'react';

export function Todo(props) {
  const { todo } = props;
  if(todo.isDone) {
    return <strike>{todo.text}</strike>;
  } else {
    return <span>{todo.text}</span>;
  }
}

export function TodoList(props) {
  const { todos } = props;
  return (
    <div className='todo'>
      <input type='text' placeholder='Add todo' />
      <ul className='todo__list'>
        {todos.map(t => (
          <li key={t.id} className='todo__item'>
            <Todo todo={t} />
          </li>
        ))}
      </ul>
    </div>
  );
}

この時点で、 プロジェクトフォルダーにindex.htmlを作り、下記のマークアップと一緒に投入することで、コンポーネントをテストできます(GitHubでシンプルなスタイルシートを参照できます)。

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="style.css">
    <title>Immutable Todo</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="bundle.js"></script>
  </body>
</html>

src/app.jsでアプリケーションのエントリーポイントも必要です。

// src/app.js

import React from 'react';
import { render } from 'react-dom';
import { TodoList } from './components';

const dummyTodos = [
  { id: 0, isDone: true,  text: 'make components' },
  { id: 1, isDone: false, text: 'design actions' },
  { id: 2, isDone: false, text: 'implement reducer' },
  { id: 3, isDone: false, text: 'connect components' }
];

render(
  <TodoList todos={dummyTodos} />,
  document.getElementById('app')
);

npm run buildを使ってコードをコンパイルし、ブラウザーでindex.htmlファイルを探し、正常に動いていることを確認してください。

ReduxとImmutableJS

ユーザーインターフェースについてはうまくいきましたので、裏にあるstateについて見てみましょう。最初はダミーデータからはじめるとよいでしょう。なぜなら、 ImmutableJSコレクションに変換するのが簡単だからです。

import { List, Map } from 'immutable';

const dummyTodos = List([
  Map({ id: 0, isDone: true,  text: 'make components' }),
  Map({ id: 1, isDone: false, text: 'design actions' }),
  Map({ id: 2, isDone: false, text: 'implement reducer' }),
  Map({ id: 3, isDone: false, text: 'connect components' })
]);

ImmutableJSマップはJavaScriptのオブジェクトと同じようには動いてくれません。コンポーネントにいくつか微調整を加える必要があります。どこでもいいので、プロパティーアクセスの前にある(todo.get('id'))の代わりに 、(todo.id)をメソッドコールにします。

Actionの設計

シェイプと構造ができましたので、次に更新するためのactionについて考えてみましょう。この場合、必要になるactionは2つだけです。1つは新たなtodoの追加で、もう1つは古いものとのトグルです。

それでは、actionを作るため関数を定義しましょう。

// src/actions.js

// succinct hack for generating passable unique ids
const uid = () => Math.random().toString(34).slice(2);

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    payload: {
      id: uid(),
      isDone: false,
      text: text
    }
  };
}

export function toggleTodo(id) {
  return {
    type: 'TOGGLE_TODO',
    payload: id
  }
}

それぞれのactionは、固有のタイププロパティとペイロードプロパティを持ったJavaScriptのオブジェクトにすぎません。タイププロパティによって、あとでactionを実行したときに、ペイロードで何をするのかを認識するのに役立ちます。

Reducerをデザインする

stateの形式と更新するためのactionが分かったので、reducerを作成します。念のため書いておきますが、reducerは1つのstateとactionを受け取り、それを使って新たなstateをつくるための関数です。

reducerの基本的構造は次の通りです。

// src/reducer.js

import { List, Map } from 'immutable';

const init = List([]);

export default function(todos=init, action) {
  switch(action.type) {
    case 'ADD_TODO':
      // ...
    case 'TOGGLE_TODO':
      // ...
    default:
      return todos;
  }
}

ADD_TODOのactionの扱いはとてもシンプルです。最後に書かれているtodoの新しいリストを戻す.push()が使えるからです。

case 'ADD_TODO':
  return todos.push(Map(action.payload));

ここで注意してほしいのは、todoオブジェクトがリストに入れられる前に、ImmutableMapに変換しているという点です。

より複雑なactionが必要になるのは、 TOGGLE_TODOの取扱いです。

case 'TOGGLE_TODO':
  return todos.map(t => {
    if(t.get('id') === action.payload) {
      return t.update('isDone', isDone => !isDone);
    } else {
      return t;
    }
  });

リストを反復したり、あるactionにマッチするidを持つtodoを探したりするときには、.map()を使います。 そのあと、1つのキーと関数を付与された.update()を呼び出します。このメソッドは、update関数に与えられていた元の値を通した結果と代替されたキーの値とともに、新たなマップのコピーを戻します。

実際のコード例を見た方が分かりやすいでしょう。

const todo = Map({ id: 0, text: 'foo', isDone: false });
todo.update('isDone', isDone => !isDone);
// => { id: 0, text: 'foo', isDone: true }

すべてをつなぐ

actionとreducerの準備ができたので、実際にstoreを作ってReactコンポーネントにつなぎましょう。

// src/app.js

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { TodoList } from './components';
import reducer from './reducer';

const store = createStore(reducer);

render(
  <TodoList todos={store.getState()} />,
  document.getElementById('app')
);

コンポ―ネントがstoreを認識する必要があります。react-reduxを使ってこのプロセスを単純化します。コンポ―ネットをラップし、storeを認知するコンテナができあがります。オリジナルの実装を変更する必要がありません。

<TodoList />コンポーネントの周りにコンテナーが必要になります。具体的に次のように書かれています。

// src/containers.js

import { connect } from 'react-redux';
import * as components from './components';
import { addTodo, toggleTodo } from './actions';

export const TodoList = connect(
  function mapStateToProps(state) {
    // ...
  },
  function mapDispatchToProps(dispatch) {
    // ...
  }
)(components.TodoList);

コンテナーをconnect関数で作ります。connect()を呼び出すときには必ずmapStateToProps()mapDispatchToProps()の2つの関数を渡します

mapStateToPropsの関数は、storeのカレントstateを引数として受け取り(このケースではtodoのリストにあたります)、戻り値を、ラップされたコンポーネントに対してstateからプロパティへのマッピングを記述するオブジェクトとみなします。

function mapStateToProps(state) {
  return { todos: state };
}

ラップされたReactコンポーネントのインスタンス上にビジュアル化すると分かりやすいでしょう。

<TodoList todos={state} />

また、mapDispatchToPropsの機能も必要になります。これはstoreのdispatchを通して、action creatorからactionを渡すときに使えるようにするためです。

function mapDispatchToProps(dispatch) {
  return {
    addTodo: text => dispatch(addTodo(text)),
    toggleTodo: id => dispatch(toggleTodo(id))
  };
}

ここでも、ラップされたReactコンポーネントのインスタンス上で、すべてのプロパティーをまとめてビジュアル化しておくと分かりやすいでしょう。

<TodoList todos={state}
          addTodo={text => dispatch(addTodo(text))}
          toggleTodo={id => dispatch(toggleTodo(id))} />

コンポーネントがaction creatorを認識できるようにマップ化したので、イベントリスナーから呼び出せようになりました。

export function TodoList(props) {
  const { todos, toggleTodo, addTodo } = props;

  const onSubmit = (event) => {
    const input = event.target;
    const text = input.value;
    const isEnterKey = (event.which == 13);
    const isLongEnough = text.length > 0;

    if(isEnterKey && isLongEnough) {
      input.value = '';
      addTodo(text);
    }
  };

  const toggleClick = id => event => toggleTodo(id);

  return (
    <div className='todo'>
      <input type='text'
             className='todo__entry'
             placeholder='Add todo'
             onKeyDown={onSubmit} />
      <ul className='todo__list'>
        {todos.map(t => (
          <li key={t.get('id')}
              className='todo__item'
              onClick={toggleClick(t.get('id'))}>
            <Todo todo={t.toJS()} />
          </li>
        ))}
      </ul>
    </div>
  );
}

コンテナは自動的にstore内の変更点を確認し、マップ化されたプロパティーのどこが変化しようと、ラップされたコンポーネントを再表示します。

最後に、コンテナが<Provider />コンポーネントを使って、ストアを認識するようにしなければなりません。

// src/app.js

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducer from './reducer';
import { TodoList } from './containers';
//                          ^^^^^^^^^^

const store = createStore(reducer);

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

最後に

ReactとReduxに関連するエコシステムはとても複雑で、初心者にとっては腰が引けるようなものでしょう。しかし良い点もあって、ほとんどすべてのコンセプトは変換可能だということです。これまでReduxアーキテクチャーの表面に触れたことすらなかった私たちも、すでにThe Elm Architectureについての学習をはじめるきかっけになったり、OmRe-frameといったClojureScriptのライブラリーをかじってみようと思ったりするところまで来ました。同様に、immutableデータの可能性のかけら程度しか見ていないとはいえ、ClojureHaskellといった言語の学習を始める準備ができました。

単にWebアプリケーション開発の現状について調べているだけの人にせよ、JavaScriptを1日中書いているようなレベルの人にせよ、アクションベースのアーキテクチャと不変のデータを扱う経験はすでに開発者にとって重要なスキルとなっており、エッセンスを学ぶにはいいタイミングです。

(原文:How to Build a Todo App Using React, Redux, and Immutable.js

[翻訳:島田理彩]
[編集:Livit

Web Professionalトップへ

WebProfessional 新着記事