React & Flummoxチュートリアル

Flummoxは、いくつもあるFlux実装のうちの1つです。

ドキュメントにはいくつかの特徴が挙げられていますが、 Isomorphicであること(サーバとクライアントのどちらでもOK!)、ES6記法に対応していることなどがあります。

今回は、簡単なTODOアプリを作りながらFlummoxの使い方を紹介しようと思います。

このチュートリアルでは、ReactやFluxの初心者を対象にしています。簡単な概要ぐらいは知っているけど、これから始めるために簡単なサンプルが欲しいというような人にちょうど良いと思いますが、React全く初めてでも順を追えばなんとなくやっていることがわかると思います。

ソースコードこちらです。

<完成イメージ>

f:id:gibachan03:20150429140911p:plain

準備

コードを書き始める前にいくつか必要となるツールがありますので紹介します。

Gulp

よく使われているタスクランナーです。この後でコードを変換する必要が出てくるので、このツールを使ってその処理を自動化させます。

こちらはグローバルな環境にインストールしておく必要があるので、無い場合は次のようにインストールしましょう。

npm install -g gulp

Browserify

JavaScriptでモジュール管理をするためのツールです。JavaScriptのファイルを複数に分けておき、必要なモジュールをrequireで取得できるようにしてくれます。

Babel

ここではJavaScriptをES6記法で記述していくのですが、実際にブラウザで動作させるにはES5に変換する必要があります。Babelはその変換をしてくれるツールです。

面倒ですが、これらのツールを使ってコードを書く準備をしましょう。 まずはnpmでReactとFlummoxをインストールし、別に開発に必要となるツールを開発環境用にインストールします。

mkdir flummox-excersize && cd flummox-excersize

npm init

npm install --save react flummox

npm install --save-dev babelify browserify gulp vinyl-source-stream

babelifyはBrowserifyでBabelの変換をしてくれるモジュールで、vinyl-source-streamは、最終的に1つのJavaScriptファイルにまとめるためのモジュールです。

インストールが済んだら、gulpfile.jsを作成し、Gulpで実行する作業を書きます。

gulp

詳細に説明はしませんが、これでコマンドにgulpと打つだけでコードの変換が行われます。 今の時点ではファイルが無いのでエラーになります。

React - Hello World -

まずはReactでブラウザにHello Worldを表示させましょう。

Appコンポーネント

アプリのルートとなるコンポーネントを/components/App.jsxとして作成します。

import React from 'react';

class App extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>
        <h1>Hello World</h1>
      </div>
    );
  }
}

export default App;

ES6記法で書いています。そのためReactのバージョンが0.13以上である必要があります。

app.js

次に、このコンポーネントを使用して描画するコードをapp.jsに書きます。

import React from 'react';

import App from './components/App.jsx';

React.render(<App />,
  document.getElementById('app'));

こちらもES6記法である以外は問題無いと思います。

index.html

最後にブラウザで表示されるindex.htmlを書きます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Flummox TODO</title>
</head>
<body>
  <div id="app"></div>

  <script src="build/bundle.js"></script>
</body>
</html>

ここで読み込んでいるbundle.jsは、gulpで変換して出力されるファイルです。

gulp

ブラウザでindex.htmlを開いて、"Hello World"が表示されたら成功です。

次からFlummoxを使って行きます。また、次からスペースの都合上コードは全て掲載しません。 必要であればサンプルコードをダウンロードしてご覧ください。

Action

Actionはビュー(Component)等で発生したイベントに応じて動作します。 モデル(Store)はActionを監視するので、Actionが動作したらそれに応じて内部のデータを変化させます。

ここではTODOアプリを作るので、TODOの新規作成、削除、終了フラグの切り替えのActionを定義します。

actions/Actions.js

class TodoActions extends Actions {
  // Todo新規作成
  createTodo(text) {
    return text;
  }

  // Todo削除
  deleteTodo(id) {
    return id;
  }

  // Todo終了フラグの切り替え
  toggleTodo(id) {
    return id;
  }
}

ActionsはFlummoxのActionsクラスを継承します。

Store

Storeはモデル(データ)を格納し、その状態を管理します。 Storeは、Actionsからの操作によりその状態を変化させ、その他からの影響は受けません。 また、Storeの状態が変化するとビュー(Component)がそれに応じて更新されます。

stores/TodoStore.js

import { Store } from 'flummox';

class TodoStore extends Store {
  ...
}

export default TodoStore;

StoreはFlummoxのStoreを継承します。

まずコンストラクタで、ActionとStoreの関連付けを行います。

constructor(flux) {
  super();

  // fluxインスタンスからActionsを取得
  const todoIds = flux.getActionIds('todo');

  // ActionとStoreメソッドを関連付け
  this.register(todoIds.createTodo, this.handleCreateTodo);
  this.register(todoIds.updateTodo, this.handleUpdateTodo);
  this.register(todoIds.deleteTodo, this.handleDeleteTodo);
  this.register(todoIds.toggleTodo, this.handleToggleTodo);

  // Storeの初期状態(初期データ)
  this.state = {
    todos: {}
  };

Actionが発生した場合、StoreはSetState状態を変化させます。

handleCreateTodo(text) {
  ...

  // 状態を変化
  this.setState({ todos });
}

setStateを呼ぶと状態が変化し、それがComponentへ伝えられる仕組みになっています。

Flux

ActionsとStoreを作ったので、これらを元にFluxインスタンスを作成します。

app.js

class Flux extends Flummox {
  constructor() {
    super();

    this.createActions('todo', Actions);
    this.createStore('todo', TodoStore, this);
  }
}

const flux = new Flux();

FluxはFlummoxを継承します。

fluxインスタンスを作成したらAppのComponentにインスタンスを渡します。

React.render(
  <FluxComponent flux={flux}>
    <App />
  </FluxComponent>,
  document.getElementById('app'));

ここで、FlummoxのFluxComponentというComponentを使用しています。 このComponentは、その配下のComponent(App)にFluxインスタンスを渡す役割があります。 つまり次のように書くのと同じイメージです。

<App flux={flux} />

ですがFluxComponentは便利な機能を持っています。 例えばAppの配下に子Componentがあり、その子ComponentもFluxインスタンスを必要とするのであれば、毎回このようにFluxインスタンスを渡してあげる必要があります。 ですがFluxComponentを使うとその記述を省くことができるようになります。 このような機能については次以降で確認できます。

Component

ComponentはStoreの状態を監視し、Storeの状態が変わったらComponentを更新します。 また、ボタンクリック等のユーザ操作に応じてActionを発生させます。

ここではComponentの階層構造を次のように考えます。

App
  - TodoEdit : Todo新規追加フォーム
  - TodoList : Todoリスト
    - TodoListItem

まず、components/App.jsxを書き換えます。

class App extends React.Component {
  ...
  render() {
    return (
      <div>
        <h1>Todo list</h1>
        <FluxComponent>
          <TodoEdit />
        </FluxComponent>
        <FluxComponent connectToStores={['todo']}>
          <TodoList />
        </FluxComponent>
      </div>
    );
  }
}

ここで、TodoEdit Componentの親としてFluxComponentを指定しています。 これにより、App Componentからfluxインスタンスが自動的に渡され、TodoEdit内でthis.props.fluxの形で使用できるようになります。

components/TodoEdit.jsx

...
handleSubmit(e) {
  e.preventDefault();
  if (this.state.newTodo.length === 0) return;

  // fluxインスタンスからActionsを取得し、Actionsを発生させる
  this.props.flux.getActions('todo').createTodo(this.state.newTodo);

  this.setState({
    newTodo: ''
  });
}
...

handleSubmitは新規Todo作成フォームのSubmitイベントハンドラです。 この中でActionを発生させ、その結果Storeの状態が変化することになります。

また、TodoList Componentの親のFluxComponentにはconnectToStoresの指定をしています。 これにより、'todo'で識別されるStore(TodoStore)がTodoListのpropsに展開されます。

components/TodoList.jsx

render() {
  var todos = this.props.todos;
  ...
}

結果

動作を確認しましょう。

gulpを実行した後にindex.htmlを開くとTodoアプリが動きます。 f:id:gibachan03:20150429140911p:plain

確認して頂きたいのはデータやイベントが常に1方向に流れていることです。

  • Todo新規作成 (ユーザがテキストを入力、ボタンクリック) => TodoActions(ceateTodo) => TodoStore => TodoList => TodoListItem

  • Todo削除 TodoListItem => (ユーザが削除ボタンクリック) => TodoActions(deleteTodo) => TodoStore => TodoList

このような構造でアプリを作る考え方がFluxというアーキテクチャであるということです。 より詳細な説明は公式をご確認ください。

まとめ

Reactを触りだすとFluxという概念が出てきます。初めてみると結構複雑そうで面倒な印象を持つかもしれません。 ですが、使ってみるとそれほど難しくないことがわかります。 なので、このチュートリアルがFlummoxもしくはReact/Fluxの最初の一歩として、ちょっとでも何かの参考になれば嬉しいです。

あと何か不足や間違いなどありましたらご連絡頂けるととてもありがたいです。 => Twitter@gibachan03