Reactの単純なサンプルでFluxの実装を解説

先日『Flux – Dispatcher【日本語訳】と実装のポイント』という記事を投稿しました。Fluxの理解を深めるために、その実装の核となるDispatcherを理解することが大事だと思ったからです。おかげで、ある程度Fluxの理解進みました。今回さらにFluxを実装することで、理解を深めたいと思い、簡単なサンプルを作ってみることにしました。このサンプルを通して、Fluxの実装方法について説明していきたいと思います。だいぶ長くなりましたが、ぜひ参考にしていただければと思います。

Fluxの実装サンプル

まずはサンプルを見てください。フォームに入力したテキストを表示させるだけのものとなっています。とても単純ですが、Fluxを使って実装しています。

Flux実装サンプルコード – GitHub

Flux実装の説明の前に

Fluxとは

flux - logo
Flux

Fluxは「クライアントサイドのWebアプリケーション(ユーザーインターフェース)を開発するためのアプリケーションアーキテクチャ」です。Fluxは役割によって“View”“Store”“Dispatcher”“Action”の4つの部品にわけられます。それぞれの役割は、よく以下の図によって説明されています。Fluxは、データの流れを一方向性にし、Reactを使ったViewの実装をよりシンプルに行えるようになしてくれます。

flux react.js

Fluxを実装するために必須な知識

できる限りサンプルは単純にしたつもりですが、それでもある程度の知識がないとFluxを実装するのは難しいです。Fluxの実装を理解する上で必要な知識を以下に挙げてみました。

  • React
  • Observerパターン、発行/購読(PubSub)パターン
  • Fluxのdispatcher.js
  • Node.jsのモジュールの使い方

Reactに関しては言わずもがなですね。FluxのViewの部分を担います。Observerパターン発行/購読(PubSub)パターンは、デザインパターンのひとつです。Fluxではデータの受け渡しにカスタムイベントを使う必要があり、この辺りの知識が必要となります。

今回特に大事になってくるのが、このFluxのdispatcher.jsです。Fluxの大部分をこのdispatcher.jsを使って実装します。まずは以下に紹介するFluxのドキュメントや当ブログの翻訳記事などを参考にポイントを抑えておくことをお勧めします。

さらに、Node.jsモジュールの使い方も知っておく必要があります。今回はできるだけ単純にしたつもりですが、それでもEventsやobject-assignといったモジュールを使うことになりました。同様に、ReactやFluxもモジュールとして読み込んで使うことになります。

Flux実装のポイント

Fluxの実装はフレームワークを使ってもいいし、自分で実装してもいいし、いろいろと方法はあります。そうした中でFacebook/Fluxが用意してくれている「dispatcher.js」を使うと比較的容易にFluxを実装することができます。以下に、dispatcher.jsを使ったFluxの実装のポイントを簡単にまとめてみました。

View: アプリケーションのUIを担う場所

  • Reactで実装。
  • onClickなどのイベントによって、Actionを呼び出す。
  • Storeの状態を監視し、状況に応じてsetState()メソッドを使ってViewを更新。

Action: Viewからのアクションに応じて適切なデータをSoterに伝える場所

  • dispatch()メソッドの引数にオブジェクトリテラル形式のデーター(ペイロード)をセット(Action Creator)。
  • Storeがアクションを識別できるようにアクションタイプ属性を持たせる。
  • ViewでActionが発生したら、対応するdispatch()メソッドを実行。

Dispatcher: Actionにおいて指定された適切な配送先(Store)にデータを配送する場所

  • dispatch()メソッドが実行されたら、引数にセットされているオブジェクト(ペイロード)をStoreに送信。

Store: Dispatcherによって送られたデータを記録し、また更新する場所

  • register()メソッドにコールパック関数を登録。
  • Dispatcherによってペイロードが送られて来たら、登録したすべてのコールバック関数を実行。
  • それぞれのコールバック関数は、ペイロードのアクションタイプ属性を見てアクションに対応すべきか否かを判断。
  • コールパック関数が複数ある場合は、waitFor()メソッドを使って順番を制御。
  • 状況に応じて、イベントを発行し、View(React)に通知。

Fluxの実装前の準備

まず、Fluxを実装するために環境を整えておく必要があります。利用するモジュールのインストール方法と読み込みの方法を説明します。前提として、npmを使うので、Node.js環境が必要です。

モジュールのインストール

fluxのインストール>

FluxのDispatcherを実装するために、こちらfluxモジュール内のDispatcherオブジェクを利用します。

$ npm install --save flux

reactのインストール>

Viewの実装をReactで行います。

$ npm install --save react

object-assignのインストール>

オブジェクト同士をマージしてくれるES2015のObject.assign()メソッドを利用するためのPolyfillです。

$ npm install --save object-assign

モジュールの読み込み

インストールしたモジュールをページに読み込みます。今回は、インストールしたモジュール以外にNode.jsのモジュールであるEventsモジュール内のEventEmitterオブジェクトも一緒に読み込みます。これはNode.js上でイベントを扱えるようにするためです。

var Dispatcher = require("flux").Dispatcher;
var EventEmitter = require("events").EventEmitter;
var assign = require("object-assign");
var React = require("react");

さらに、読み込んだDispatherオブジェクトに関しては、newしてインスタンスを生成します。

var testDispatcher = new Dispatcher();

これでFluxが書けるようになりました。さっそくFluxの実装について説明していきます。

Fluxの「View」の実装(Reactだけでの実装)

上記サンプルをFluxを使わずにReactだけで実装すると以下のようになります。これが、FluxにおけるViewの元となります。Viewは、アプリケーションのUIを担う場所となり、通常はReactで実装することが推奨されています。(背景色を変えている部分がこの後のFluxの実装において、変更していく箇所となります。)

// View

var TestApp = React.createClass({
  getInitialState: function () {
    return {value: null};
  },
  setInputVal: function (testValue) {
    this.setState({value: testValue});
  },
  render: function () {
    return (
      <div className="testApp">
        <TestForm onClickBtn={this.setInputVal} />
        <TestDisplay data={this.state.value} />
      </div>
    );
  }
});

var TestForm = React.createClass({
  send: function (e) {
    e.preventDefault();
    var testValue = React.findDOMNode(this.refs.test_value).value.trim();
    this.props.onClickBtn(testValue);
    React.findDOMNode(this.refs.test_value).value = "";
    return;
  },
  render: function () {
    return (
      <form>
        <input type="text" ref="test_value" />
        <button onClick={this.send}>送信</button>
      </form>
    );
  }
});

var TestDisplay = React.createClass({
  render: function () {
    var message = this.props.data;
    return (
      <div>{message}</div>
    );
  }
});

React.render(
  <TestApp />,
  document.getElementById("content")
);
View (React)

Reactに関しては、Reactのサイトや当ブログのチュートリアルの翻訳記事などを参考にしてください。

Fluxの「Action」の実装

次にActionの実装の説明に移ります。ActionはViewからのアクションに応じて(状況によりサーバと通信を行い)適切なデータをSoterに伝える場所です。TestActionオブジェクトを作り、Viewから実行できるメソッド(Action Creator)を定義します。ここでは便宜的にtest()メソッドとしました。Action自体は、actionTypeとStoreに渡したい値を持ったオブジェクトとなります。

var TestAction = {
  test: function (testValue) {
    testDispatcher.dispatch({
      actionType: "test",
      value: testValue
    });
  }
};
Action

View上でtest()メソッドが実行されると、testDispatcherインスタンスのdispatch()メソッドが実行されるようにするのがポイントです。

この後説明しますが、Storeで実装するtestDispatcherインスタンスのregister()メソッドは、dispatch()メソッドを実行すると、その引数にセットしたActionの値(オブジェクト/ペイロード)を受け取ってくれるようになっています。

Store内には処理の内容に合わせて複数のregister()メソッドを実装する場合もあります。dispatch()メソッドが実行されると、実装したすべてのregister()メソッドがdispatch()メソッドの引数にセットした値を受け取るようになっています。従って、それぞれのregister()メソッド内で実行する処理の出し分けができるように、dispatch()メソッドの引数にセットするオブジェクトには、識別用に"actionType"というキーを設けておくのがポイントとなります

View(React)の修正

Actionを実装したことで、React内で行っていたクリックイベントによる処理が必要なくなります。Viewを修正していきます。

まずは、form部分を形成しているTestFormコンポーネントから。ボタンがクリックされた時に呼ばれるsend()メソッド内で実行する関数をActionで定義したTestAction.test()メソッドに置き換えます。

var TestForm = React.createClass({
  send: function (e) {
    e.preventDefault();
    var testValue = React.findDOMNode(this.refs.test_value).value.trim();
    this.props.onClickBtn(testValue); // 削除
    // Actionで定義したTestAction.test(testValue)メソッドを実行
    TestAction.test(testValue); // 追加
    React.findDOMNode(this.refs.test_value).value = "";
    return;
  },
  render: function () {
    return (
      <form>
        <input type="text" ref="test_value" />
        <button onClick={this.send}>送信</button>
      </form>
    );
  }
});
View - TestFormコンポーネント

ボタンがクリックされた時の処理をActionで行うようにしたので、rootのTestAppコンポーネント内で定義していた処理は削除します。

var TestApp = React.createClass({
  getInitialState: function () {
    return {value: null};
  },
  setInputVal: function (testValue) {
    this.setState({value: testValue});
  },
  render: function () {
    return (
      <div className="testApp">
        <TestForm onClickBtn={this.setInputVal} />
        <TestDisplay data={this.state.value} />
      </div>
    );
  }
});
View - TestAppコンポーネント

Fluxにおける非同期処理

ここでは詳しく書きませんが、Fluxで非同期処理を行いたい場合は、すべてこのActionの部分で行うように実装します。HTTPによるサーバとのやり取りも非同期処理となるので、すべてここで行うようにします。

Fluxの「Store」の実装

Storeの実装の説明に移ります。StoreはDispatcherによって送られたデータを記録し、また更新する場所です。まず、Storeで扱うデータを保存するための_testオブジェクトを定義します。Storeでは、基本的にこの_testオブジェクトの値を扱うための処理を書いていくことになります。

var _test = {value: null};
Store

次に、TestStoreオブジェクトを作り、上で定義した_testオブジェクトを扱う処理を書いていきます。まずはベースとなる部分から書いていきます。ここでは、register()メソッドを使って、Action内で実行されたdipatcher()メソッドの引数にセットされた値を取得し、_testオブジェクトの値を変更するような処理となっています。

var TestStore = {
  // "payload"によって、dispatch()メソッドの引数にセットされた値を取得
  dispatcherIndex: testDispatcher.register(function (payload) {
    // "actionType"識別子による処理の判別
    if (payload.actionType === "test") {
      _test.value = payload.value;
    }
  })
};
Store

testDispatcherインスタンスのregister()メソッドは、Action内のdispatch()メソッドが実行されると、自動的にdispatch()メソッドの引数にセットした値を取得し、register()メソッドの引数にセットしたコールバック関数が実行されるようになっています。

Store内にregister()メソッドを複数書く場合

Store内にはregister()メソッドを複数書くことができます。その場合、Action内のdispatch()メソッドが実行されるとすべてのregister()メソッドでdispatch()メソッドによる値を取得するようになります。Actionの説明の部分でも書きましたが、dispatch()メソッドの引数にセットしたオブジェクトに定義した"actionType"の値を見て、register()メソッド内の処理を実行させるようにするのがポイントです。

ここでは詳しく書きませんが、register()メソッドが実行されると、戻り値としてトークンを返します。FluxのDispatcherにはwaitFor()メソッドというものが用意されていて、そのwaitFor()メソッドにトークンを渡すことによって、register()メソッドの実行順序をコントロールすることも可能になっています。

Fluxの「Store」の実装その2(ViewでStoreの値を取得する)

現状ではViewとStoreは別々のオブジェクトであり結合されていない状態なので、StoreからViewへデータを渡す仕組みが必要となってきます。

Storeの値を取得するメソッドを作成

まず、StoreからViewへ_testオブジェクトを渡すためのgetAll()メソッドを作ります。このメソッドをView上で(getInitialState()メソッドやsetState()メソッドを通して)実行することで、StoreのデータをView上で取得できるようになります。

var TestStore = {
  getAll: function () {
    // _testオブジェクトを返す
    return _test;
  },
  dispatcherIndex: testDispatcher.register(function (payload) {
    if (payload.actionType === "test") {
      _test.value = payload.value;
    }
  })
};
Store

ViewのrootコンポーネントのgetInitialState()メソッドの処理の部分をgetAll()メソッドが実行されるように修正しておくと、初期表示時に_testオブジェクトの値が使われるようになります。

var TestApp = React.createClass({
  getInitialState: function () {
    return {value: null}; // 削除
    return TestStore.getAll(); // 追加
  },
  render: function () {
    return (
      <div className="testApp">
        <TestForm />
        <TestDisplay data={this.state.value} />
      </div>
    );
  }
});
View - TestAppコンポーネント

Storeの状態の変化をViewに伝える

次に、_testオブジェクトの値が変更されたタイミングで、Viewを変更させるようにしていきます。Viewの値を変更するには、View内でsetState()メソッドを実行させることで可能となります。なのでここでの実装のポイントは、_testオブジェクトの値が変更されたかをView側でどのようにして取得するか」ということになります。

ここで上記で読み込んだNode.jsのモジュールであるEventsモジュールのEventEmitterオブジェクトを使用していきます。このEventEmitterは、Node.js上でのカスタムイベントの実装を可能にします。いわゆるObserverパターンやPub/Subパターンを実装する際に役立ちます。

TestStoreオブジェクトにassign()メソッドを使ってEventEmitterオブジェクトをマージすると、TestStoreオブジェクト内でEventEmitterオブジェクトが扱えるようになります。そのようにして、Store内にカスタムイベントを実装していきます。詳細はソース内のコメントを見てください。

var _test = {value: null};

var TestStore = assign({}, EventEmitter.prototype, {
  getAll: function () {
    return _test;
  },
  // イベントを発生させるメソッドの定義
  emitChange: function () {
    // イベント名"change"としてイベントを発生
    this.emit("change");
  },
  // イベントの監視(購読)とコールバックの定義
  addChangeListener: function (callback) {
    // "change"イベントの発生を取得したら、引数にセットされたコールバック関数を実行
    this.on("change", callback);
  },
  dispatcherIndex: testDispatcher.register(function (payload) {
    if (payload.actionType === "test") {
      _test.value = payload.value;
      // emitChange()メソッドを実行(イベント発生)
      TestStore.emitChange();
    }
  })
});
Store

Store内で_testオブジェクトの値を変更したら、TestStore.emitChange()メソッドが実行され、"change"イベントが発生するようになりました。View側でこれを受け取り、setState()メソッドを実行し、Viewを再描画するようにします

View側の実装は以下の通りです。ReactのcomponentDidMount()メソッドを使って、TestStore.addChangeListener()メソッドにsetState()メソッドが実行されるコールバック関数を登録します。setState()メソッドの引数にはTestStore.getAll()メソッドをセットしておきます。

var TestApp = React.createClass({
  getInitialState: function () {
    return TestStore.getAll();
  },
  // コンポーネントが描画されたら実行
  componentDidMount: function() {
    var self = this;
    // TestStoreのaddChangeListener()メソッドにコールバック関数をセットし実行
    TestStore.addChangeListener(function () {
      // TestStore.getAll()を引数にセットし、setState()メソッドを実行
      // →Viewが再描画される
      self.setState(TestStore.getAll());
    });
  },
  render: function () {
    return (
      <div className="testApp">
        <TestForm />
        <TestDisplay data={this.state.value} />
      </div>
    );
  }
});
View - TestAppコンポーネント

componentDidMount()メソッドは、初期描画が発生した直後に一度実行されるメソッドです。イベントリスナーなどを使ったイベントの登録などに使用されます。

Fluxの「Dispatcher」の実装

Dispatcherは、Actionにおいて指定された適切な配送先(Store)にデータを配送する場所です。今回、詳しい実装の説明をしなかったと思いますが、それはFluxモジュールのDispatcher.jsを使っているためです。機能的にはActionとStoreに内包されていると考えてもらえばよいです。

Fluxで実装したサンプルのソースコード

これでFluxによる上記サンプルの実装が可能となります。最後に、これまで説明してきたコードをひとつにまとめておきます。Fluxは、アクションタイプ名やイベント名を定数で書くのが通例のようなので、上記では複雑にならないように敢えて使いませんでしたが、以下では定数を使うようにしています。

var Dispatcher = require("flux").Dispatcher;
var EventEmitter = require("events").EventEmitter;
var assign = require("object-assign");
var React = require("react");

// Dispatcher
var testDispatcher = new Dispatcher();

// Action
var testConstants = {
  TEST: "test"
};

var TestAction = {
  test: function (testValue) {
    testDispatcher.dispatch({
      actionType: testConstants.TEST,
      value: testValue
    });
  }
};

// Store
var CHANGE_EVENT = "change";
var _test = {value: null};

var TestStore = assign({}, EventEmitter.prototype, {
  getAll: function () {
    return _test;
  },
  emitChange: function () {
    this.emit(CHANGE_EVENT);
  },
  addChangeListener: function (callback) {
    this.on(CHANGE_EVENT, callback);
  },
  dispatcherIndex: testDispatcher.register(function (payload) {
    if (payload.actionType === testConstants.TEST) {
      _test.value = payload.value;
      TestStore.emitChange();
    }
  })
});

// View
var TestApp = React.createClass({
  getInitialState: function () {
    return TestStore.getAll();
  },
  componentDidMount: function() {
    var self = this;
    TestStore.addChangeListener(function () {
      self.setState(TestStore.getAll());
    });
  },
  render: function () {
    return (
      <div className="testApp">
        <TestForm />
        <TestDisplay data={this.state.value} />
      </div>
    );
  }
});

var TestForm = React.createClass({
  send: function (e) {
    e.preventDefault();
    var testValue = React.findDOMNode(this.refs.test_value).value.trim();
    TestAction.test(testValue);
    React.findDOMNode(this.refs.test_value).value = "";
    return;
  },
  render: function () {
    return (
      <form>
        <input type="text" ref="test_value" />
        <button onClick={this.send}>送信</button>
      </form>
    );
  }
});

var TestDisplay = React.createClass({
  render: function () {
    var message = this.props.data;
    return (
      <div>{message}</div>
    );
  }
});

// render
React.render(
  <TestApp />,
  document.getElementById("content")
);

このサンプルのソースコードはGitHubにもアップしていますので、git cloneでもしてお試ししていただければと思います。使用方法などはGitHubのREADMEに書いてあります。

まとめ

単純なサンプルによるFluxの説明ということで、簡単に書けるかと思ったら、かなりのボリュームとなってしましました。自分はやっとFluxについて学び始めたところですが、FluxのDispatcher.jsを理解することが、Fluxの理解につながったと思っています。fluxxorReduxなどFluxを採用したフレームワークなどもあり、比較的容易にFluxでのReactの実装もできるようになっていますが、やはりしっかりFluxの仕組みを理解していおくことが大事かなと思っています。

最後に、参考にさせていただいたサイトと書籍を紹介しておきます。

参考サイト

参考書籍

伊藤直也さんによるFluxの詳しい解説記事(全9頁)があります。とても貴重な情報源となると思います。

WEB+DB PRESS Vol.87
  • 『WEB+DB PRESS Vol.87』
  • 著者: 佐藤鉄平(著), 小林明大(著), 石村真吾(著), 坂上卓史(著), 上原誠(著), 他13名
  • 出版社: 技術評論社
  • ページ数: 168ページ
  • 発売日: 2015年7月25日
  • ISBN: 4774173703

コメント一覧

  1. ありがとうございました。大変助かりました。

    morita  返信

    • moritaさん

      コメントありがとうございます。こちらこそお役に立てたということで嬉しいです。私も最近になってやっとReactやFluxをメインで使うようになってきました。まだまだ勉強中です。

      何かあれば、またコメントなどいただけると幸いです。今後ともよろしくお願いいたします!

      Takanori Maeda  返信

  2. かつてないほどdispatcherのdispatchとregisterの関係がわかりやすい(個人的には唯一理解できた)チュートリアルでした。
    しばらく足踏みしていたFluxの一歩をようやく踏み出せそうです。
    ありがとうございます。

    zuzu  返信

    • zuzuさん

      コメントありがとうございます。返信がだいぶ遅くなりました。

      dispatcherはfluxを理解する上でとても重要な部分だと思います。dispatchとregisterの関係などちゃんと理解しておけば、他のfluxフレームワクークもそんなに苦労せずに使えるようになるかと思います。

      何かあれば、またコメントなどいただけると幸いです。今後ともよろしくお願いいたします!

      Takanori Maeda  返信

  • 必須

コメント