Flux UtilsでのFlux実装方法を超シンプルなサンプルを使って解説

先日『Flux Utilsのドキュメント日本語訳』というFlux Utilsのドキュメント英語版を翻訳した記事を投稿しました。Flux Utilsは、Facebook製のFluxフレームワークとでもいうでしょうか。ただ残念なことに、このドキュメントを読むだけでは、Flux Utilsの使い方のイメージを完全に掴むことは難しいと思います(自分がそうでした…)。ということで、Flux Utilsを使って、簡単なサンプルを作ってみました。この簡単なサンプルを通して、Dispatcher.jsだけを使ったFluxの実装方法と比較しながら、Flux Utilsの使い方を説明していきます。

はじめに(Flux Utilsとは)

flux-logo

Flux Utilsは、Facebook製のFluxフレームワークとなります。ただ、すべての用途に対応するような完成されたフレームワークではないとFlux Utilsのドキュメントには書いてあります。Flux Utilsで対応できないケースが発生したら、Reduxなど他のFluxフレームワークを使ってくださいというスタンスのものです。

詳細は、Flux Utilsのドキュメントをご確認ください。

Flux Utilsの特徴

Flux Utilsの大きな特徴は、「Store」「Container」になるでしょう。Fluxは、いわゆる「View」「Action」「Dispatcher」「Store」の4つの部位で構成されるアーキテクチャと説明されますが、Flux Utilsはこの「Store」と「View」の部分を強化するユーティリティとなります。

Flux Utilsで用意されているユティリティクラス

<Store向けユーティリティクラス>
  • Storeクラス: dispatcherを受け取り、storeのインスタンスの作成と登録を行うベースクラス
  • ReduceStoreクラス: stateを作成/保持し、actionをreduceしてstateを更新するクラス(Storeクラスを継承している)
  • MapStoreクラス: stateをimmutableなmapとして定義するクラス(ReduceStoreを継承している)
<View向けユーティリティクラス>
  • Container: View(React)のルートComponentをセットし、Viewをコントロールするクラス。関連のあるStoreに変更があった際にそのstateを更新し、Viewに変更を通知する(ReactのsetState()メソッドのような振る舞い)。

Dispatcher.jsだけで実装する場合との違い

Facebookが用意してくれいてるDispatcher.jsのみでも、Fluxの実装は可能です。当ブログでも、Dispatcher.jsを使ったFluxの実装方法について以下の記事を投稿しています。

Dispatcher.jsのみでFluxを実装する場合と、Flux Utilsを使ってFluxを実装する場合での大きな違いは、簡単に言うとStoreからViewへデータを渡す仕組みを自分で作る必要がないということになります。Dispatcher.jsのみでFluxを実装する場合は、Storeのstateに変更があった際は、Store側でカスタムイベントを発火 => View側でイベントを監視 => setState()を実行しViewを再描画といった一連の処理を書く必要があります。

この辺の違いがわかるように、この後のサンプルの説明では、Dispatcher.jsのみでFluxを実装する場合と、Flux Utilsを使ってFluxを実装する場合を比較しながら行っていきます。

サンプル

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

以下は、上記のサンプルをFlux Utils、Dispatcher.jsのそれぞれで実装したサンプルコードです。簡単に説明するために、ファイルの分割などはせず、1つのファイルに全部書いています。参考にどうぞ。

環境の準備

まずは、環境の構築から説明していきます。基本的にnpmを使った方法となります。ここでは説明を省きますが、事前にNode.js環境を用意しておく必要があります。

インストール

以下のコマンドを使って、React(View用)とFluxをnpm経由でインストールします。

Reactのインストール

$ npm install --save react react-dom
CLI

Fluxのインストール

$ npm install --save flux
CLI

インポート

次に実際にコードを書いていくためのapp.jsファイルを作成し、インストールしたReactとFluxをファイルにインポートします。Fluxに関しては、fluxモジュールからDispatcherクラス、flux/utilsモジュールからReduceStoreクラスとContainerクラスを今回はインポートするようにします。

import React, { Component } from 'react';
import { render } from 'react-dom';
import { Dispatcher } from 'flux';
import { ReduceStore, Container } from 'flux/utils';
app.js

JSXとES6のコンパイル環境の準備

今回のコードはES6(ES2015)構文で書いています。最終的にブラウザで読み込めるES5に変換する必要があります。同時にReactで使うJSXの変換も必要です。そうしたコンパイル環境の構築も必要です。詳細は当ブログの以下の記事を参考にしてください。


それでは、早速Flux UtilsでのFluxの実装方法について説明していきます。

Dispatcherの実装

「Dispatcher」は、Viewで実行されたActionを受け取り、Actionにおいて指定された適切な配送先(Store)にデータを配送する場所となります。

Dispatcher.jsのみでの実装の場合も、Flux Utilsでの実装の場合も、Dispatcherの実装は以下のみです。Dispatcherモジュール(dispatcher.js)を読み込むだけでOKです。(Flux Utilsを使う場合は、Storeのインスタンスを生成する際に、コンストラクタの引数として渡します。)

const dispatcher = new Dispatcher();
Dispatcherの実装

Actionの実装

「Action」は、Viewで実行されたActionに応じて、適切なデータをSoterに伝える場所です。Viewで実行されるAction(setterではなく、あくまでもユーザーのアクション)を定義します。

ViewでActionが実行されたら、対応するDispatcherのdispatch()メソッドを実行させます。Storeに送るデーター(ペイロード)は、dispatch()メソッドの引数に、オブジェクトリテラル形式でセットします。その際にStoreがActionを識別できるように、Actionごとに名前(アクションタイプ属性)をつけておきます。

Dispatcher.jsのみでの実装の場合も、Flux Utilsでの実装の場合も、実装方法は同じです。

// Action名を定義
const act = {
  SEND: 'send'
};
// FormActionオブジェクトとして、各Action内容を定義
// 処理内でActionの「名前(type)」と「データ(value)」を
// 上記で実装したdispatcherインスタンスのdispatch()メソッドに渡すようにする
const FormAction = {
  send(val) {
    dispatcher.dispatch({
      type: act.SEND,
      value: val
    });
  }
};
Actionの実装

サーバーとの非同期通信は全てこのActionの部分で行うようにします。

Storeの実装

「Store」は、Dispatcherによって送られたデータを記録し、更新する場所です。Dispatcherからの特定のactionに応答し、データに変更があったら変更をemit(発火)させます。View(Container)がデータを取得できるようにpublicなgetterを持ちます。

(比較)Storeを「Dispatcher.jsのみ」で実装する場合

Dispatcher.jsのみでStoreを実装する場合は、すごく複雑でした。StoreからViewへデータを渡す仕組みを自分で作る必要がありました。

import { EventEmitter } from 'events';
import assign from 'object-assign';

const CHANGE_EVENT = 'change';
var stateData = {value: null};

const FormStore = assign({}, EventEmitter.prototype, {
  getAll() {
    return stateData;
  },
  emitChange() {
    this.emit(CHANGE_EVENT);
  },
  addChangeListener(callback) {
    this.on(CHANGE_EVENT, callback);
  },
  dispatcherIndex: dispatcher.register((action) => {
    switch (action.type) {
      case: act.SEND:
        stateData.value = action.value;
        FormStore.emitChange();
    }
  });
});
Dispatcher.jsのみでのStoreの実装

Dispatcher.jsのみでのStoreの実装方法の詳細については以下をご参照ください。

Storeを「Flux Utils」で実装する場合

Flux Utilsを使うとシンプルにStoreを実装することができます。今回はFlux UtilsのReduceStoreを使ってサブクラスを定義します。getInitialState()メソッドを使ってStoreの初期stateを作成し(ReactのgetInisialState()みたいなものです)、reduce()メソッドでDispatcherによって送られたデータを受け取り、受け取ったアクションタイプ属性を見て、対応するActionを実行します。

class FormStore extends ReduceStore {
  // Storeの初期stateを作成
  getInitialState() {
    return {
      'value': null
    };
  }
  // reduce()メソッドを定義
  reduce(state, action) {
    // 受け取ったアクションタイプ属性を見て
    // 対応するActionを識別
    switch (action.type) {
      case act.SEND:
        return {
          'value': action.value
        };
    }
  }
};
Flux UtilsでのStoreの実装

定義したReduceStoreのサブクラスをnewします。その際に、対応するDispatcherのインスタンスを引数にセットします。

const formStore = new FormStore(dispatcher);
Storeのインスタンス生成

stateに変更があった場合は、ReduceStoreが自動的にStateの変更をemitしてくれるようになっています。またReduceStoreは、getState()メソッドというstateを公開するためのgetterを持っています(Viewの更新時に使用します)。

Viewの実装

Viewは、アプリケーションのUIを担う場所です。主にReact.jsを使って実装します。UIとレンダリングのロジックのすべてを持ち、propsを通してすべての情報とコールバックを受け取るようにします。

まずはReactのRootコンポーネントを「Dispatcher.jsのみ」で実装する場合と、「Flux Utils」で実装する場合を見てみることにします。

(比較)Viewを「Dispatcher.jsのみ」で実装する場合

Dispatcher.jsのみでViewを実装する場合は、若干複雑でした。Storeのイベントを監視して、リスナーを実行する仕組みを自分で作る必要がありました。

const FormApp = React.createClass({
  getInitialState() {
    return FormStore.getAll();
  },
  componentDidMount() {
    FormStore.addChangeListener(() => {
      this.setState(FormStore.getAll());
    });
  },
  render() {
    return (
      <div>
        <FormInput />
        <FormDisplay data={this.state.value} />
      </div>
    );
  }
});
Dispatcher.jsのみでのView(Rootコンポーネント)の実装

Dispatcher.jsのみでのViewの実装方法の詳細については以下をご参照ください。

Viewを「Flux Utils」で実装する場合

Flux Utilsを使うとだいぶわかりやすくViewを実装することができます。ポイントはContainerによってコントロールされるようにすることです。そのためにRootコンポーネントには、Static(静的)なgetStores()メソッドとcalculateState()メソッドを持たせるようにします。

class FormApp extends Component {
 // 監視するStoreを指定
  static getStores() {
    return [formStore];
  }
  // Storeのstateを取得してViewを更新
  static calculateState(prevState) {
    return formStore.getState();
  }
  render() {
    return (
      <div>
        <FormInput />
        <FormDisplay data={this.state.value} />
      </div>
    );
  }
};
Flux UtilsでのView(Rootコンポーネント)の実装

getStores()メソッドで、監視するStoreを指定して、calculateState()メソッドでStoreのstateを取得して、stateに変更があった場合Viewの更新(setState)を行います。

Viewの子コンポーネントの実装

Viewの子コンポーネントは、「Dispatcher.jsのみ」で実装する場合も、「Flux Utils」で実装する場合も、普通にReactで実装します。

// テキストフォームとボタンの実装
class FormInput extends Component {
 // ボタンがクリックされた時に実行される処理を定義
  _send(e) {
    e.preventDefault();
    // FormAction(Action)のsend()メソッドを実行
    FormAction.send(this.refs.myInput.value.trim());
    // テキストフォームをクリア
    this.refs.myInput.value = '';
    return;
  }
  render() {
    return (
      <form>
        <input type="text" ref="myInput" defaultValue="" />
        <button onClick={this._send.bind(this)}>Send</button>
      </form>
    );
  }
};

// 入力されたテキストを表示する部分を実装
class FormDisplay extends Component {
  render() {
    return (
      <div>{this.props.data}</div>
    );
  }
};
子コンポーネントの実装

Containerの実装

「Container」は、ViewをコントロールするReactコンポーネントです。主な役割は関連のあるstoreに変更があった時に、そのstateをupdateすることです。propsやUIロジックは持たないようにします。

ReactのRootコンポーネントをContainerに変換するために、create()メソッドの引数にセットします。

const FormAppContainer = Container.create(FormApp);
Containerの実装

レンダリング

react-domのrenderでレンダリングします。その際には、Containerに変換したReactコンポーネトを指定するようにします。

render(
  <FormAppContainer />,
  document.getElementById('content')
);

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

最後に、これまで説明してきたコードをひとつにまとめておきます。

import React, { Component } from 'react';
import { render } from 'react-dom';
import { Dispatcher } from 'flux';
import { ReduceStore, Container } from 'flux/utils';

// Dispatcher
const dispatcher = new Dispatcher();

// Action
const act = {
  SEND: 'send'
};

const FormAction = {
  send(val) { // ...[6]
    dispatcher.dispatch({
      type: act.SEND,
      value: val
    });
  }
};

// Store
class FormStore extends ReduceStore {

  getInitialState() {
    return {
      'value': null // ...[1]
    };
  }

  reduce(state, action) {
    switch (action.type) { // ...[7]
      case act.SEND:
        return {
          'value': action.value // ...[8]
        };
    }
  }

};

// Storeのインスタンス生成
const formStore = new FormStore(dispatcher);

// View (React Component)
class FormApp extends Component {
  static getStores() {
    return [formStore]; // ...[9]
  }
  static calculateState(prevState) {
    return formStore.getState(); // ...[10]
  }
  render() {
    console.log(this.state);
    return (
      <div>
        <FormInput />
        <FormDisplay data={this.state.value} /> // ...[11]
      </div>
    );
  }
};

class FormInput extends Component {
  _send(e) { // ...[4]
    e.preventDefault();
    FormAction.send(this.refs.myInput.value.trim()); // ...[5]
    this.refs.myInput.value = '';
    return;
  }
  render() {
    return (
      <form>
        <input type="text" ref="myInput" defaultValue="" /> /* ...[2] */
        <button onClick={this._send.bind(this)}>Send</button> /* ...[3] */
      </form>
    );
  }
};

class FormDisplay extends Component {
  render() {
    return (
      <div>{this.props.data}</div> /* ...[12] */
    );
  }
};

// Container
const FormAppContainer = Container.create(FormApp);

// ReactDom
render(
  <FormAppContainer />,
  document.getElementById('content')
);
Flux Utilsで実装したサンプルのソースコード(app.js)

簡単に処理の流れを書いておきます。

  1. Storeの初期stateを作成
  2. Viewのテキスト入力フォームにテキストが入力される
  3. Viewのフォームの送信ボタンが押される
  4. Viewの_send()メソッドが実行される
  5. フォームに入力されたテキストを引数にセットしてFormAction(Action)のsend()メソッドを実行
  6. Actionのsend()メソッドにより、Viewから送られたデータとアクションタイプ属性を引数にセットし、Dispatcherのdispatch()メソッドを実行
  7. Dispatcherによって送られたデータをStoreで受け取り、対応するActionを実行
  8. Storeのstateを更新
  9. Viewで関連のあるStore(formStore)を監視
  10. formStore(Store)のstateを取得して、stateに変更があった場合Viewの更新
  11. 更新されたデータをViewの子コンポーネントに渡す
  12. propsで渡された値を取得しViewに表示

まとめ

とてもシンプルなサンプルですが、Fluxで実装するとかなりボリュームのあるコード量となります。書くのは大変かと思いますが、一度書いてしまえば、すごくデータの流れなども把握しやすく見通しの良いコードになるかと思います。

特に今回取り上げたFlux Utilsを使うと、StoreとView(Container)の連携の部分がとてもシンプルに書けるようになります。この部分はFlux Utilsを使うメリットになるのではないでしょうか。それから今回は使いませんでしたが、MapStoreを使うと、stateをimmutableなmapとして定義することができるようになるので、大規模な開発を行う際には役に立つのではないでしょうか。

Fluxのフレームワークと言ったら、Redux一択といった流れになってきていますが、Flux Utilsも用途によっては十分に使えるフレームワークだと思います。とは言うものの自分はまだReduxに関してはほとんどわかっていない状態です。ある程度Fluxについては理解してきたので、次はいよいよReduxにでも手を出してみようかと思っているところです。

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

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

参考サイト

参考書籍

伊藤直也さんによるFluxフレームワークの一つであるfluxxorを使ったFluxの詳しい解説記事(全9頁)があります。Flux Utilsとの実装方法の違いなど比べてみるもの面白いかと思います。

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

コメント一覧

  • 必須

コメント