[初心者向け]Fluxを一周する

JavaScript

Fluxとは

Facebook社が提唱した、UI管理のためのアーキテクチャです。Webアプリケーションに限らず、様々なフロントエンド環境に適用できる考え方です。 54858c17-28e1-bcf3-0036-3f75d4354f7b
画像出典 https://facebook.github.io/flux/docs/in-depth-overview.html#content

この記事ではFluxそのもの詳細には触れません。英語・日本語を問わず良質な記事がたくさんあるので、気になった方は検索してみてください。

今回は、ネット上にたくさんあるドキュメントやWebサイトを参考にして、Fluxを一通りやってみた内容になります。

重要なのは、常に以下の順序で動作し、例外は無いということです。

  1. Actionが発生する
  2. Storeの値が変更される
  3. Viewが書き換えられる

最初にレンダリングされたあと、マウス操作などでActionが発行されると、設定したとおりにStoreの値が変更され、Viewの変更までが行なわれます。 ViewからStoreの値を変更するのではなく、Actionを実行することで結果としてStoreが変更されるという状態を維持します。

なお、ReactJSは上の図のViewのみを担当します。

Fluxエコシステムについて

Flux自体は考え方に過ぎず、多くの実装が公開されています。その中でも以下の2つの実装が有名です。

Reduxはサードパティ製のライブラリでしたが、最近確認したらreactjs organizationに入っていました。デファクトスタンダードだったものが、認知されてスタンダードになったのかもしれません。
アプリケーションを作るときに必要な機能が全て入っているそうで、ドキュメントもちゃんと整備されているようです。

一方のfluxは、本家Facebookによるリファレンス実装です。ドキュメントにも、最低限の機能しかないという旨が書かれています。
flux自体はDispatcherの機能しかありませんが、最近公開されたflux/utilsにはStoreなどの機能が追加されています。

今回は学習のため、簡単そうなfluxを使ってみます。以後、概念をFlux、実装をfluxと表記します。

実装

準備

環境を整備して、以下の準備をしてください。

  • create-react-appでreactjsアプリケーションを作成して、動作確認までを行なう
  • yarn add flux immutableを実行する

今回はWebSocketサーバに接続するサンプルを試してみました。サーバ側のコードは割愛します。

Containerを導入する

Fluxの図にはありませんが、fluxにはContainerという機能があります。これは、Storeのstateをview用のpropsに変換するものです。Viewと同様、React.Componentを継承したクラスを作成します。UI管理やstate操作をしない、各Componentの親になるComponentのようなものです。

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import AppContainer from './AppContainer';
import './index.css';

// ただContainerをレンダリングするだけです。
// 実際のViewは、Containerの中でレンダリングしてます。
ReactDOM.render(
  <AppContainer />,
  document.getElementById('root')
);

AppContainer.js

import React, {Component} from 'react';
import {Container} from 'flux/utils';
import AppView from './AppView';
import ConnectionStore from './ConnectionStore'; // WebSocketでつながるアプリケーションを想定してます。

class AppContainer extends Component { // Viewと同様、React.Componentを継承します
  // stateの変更を監視するstoreを持つ、flux/utilsのReduceStoreを継承したクラス登録します
  static getStores() {
    return [
      ConnectionStore,
    ];
  }

  // stateのgetterを登録します
  static calculateState() {
    return {
      connection: ConnectionStore.getState(),
    };
  }

  // ContainerはあくまでViewです。ただ他の Viewの親であるというだけです。
  // なので、ここで他のComponentをレンダリングします。
  render() {
    // AppViewでstateの値をpropsとして受け取れるように渡します。
    return <AppView connection={this.state.connection}/>
  }
}

export default Container.create(AppContainer);

AppView.js

import React, { Component } from 'react';
import ServerActions from './ServerActions'; // アプリ独自のActionです。
import './App.css';

class AppView extends Component {
  handleClick(e) {
    e.preventDefault();
    ServerActions.connect();
  }

  render() {
    return (
      <div className="App">
        // onClickでActionを呼び出します。
        <button id="connect" onClick={this.handleClick.bind(this)}>connect</button><br/>
        // Containerで渡された値(もとはstate)は、このComponentではpropsとして参照できます。
        <div>MESSAGE: {this.props.connection.get('message')}</div>
      </div>
    );
  }
}

export default AppView;

DispatcherとStoreとActionを作成する

AppDispatcher.js

import {Dispatcher} from 'flux';

export default new Dispatcher();

ConnectionStore.js

import {ReduceStore} from 'flux/utils';
import Immutable from 'immutable';
import AppDispatcher from './AppDispatcher';
import ServerActionTypes from './ServerActionTypes';

class ConnectionStore extends ReduceStore {
  // DispatcherにStoreを登録します。
  constructor() {
    super(AppDispatcher);
  }

  // 初期のStateを作成します。ここで返したオブジェクトに、reduceメソッド内からstateという名前でアクセスできます。
  // ここではImmutable.jsを使って値を作成しています。
  // MapStoreというものがImmutableなMapを作成する役割を担っていたらしいのですが、しばらく前にAPIが削除されてました。
  // https://github.com/facebook/flux/pull/405
  getInitialState() {
    return Immutable.Map({
      ws: null,
      message: 'Initialized',
    });
  }

  // actionを参照してstateを更新する対応表を実装します
  reduce(state, action) {
    switch (action.type) {
      case ServerActionTypes.CONNECTED:

        state.set({ws: action.ws});

        // 新しいstateを返します。
        // stateメソッドで得られる既存のImmutable.Mapではなく、Immutable.jsのupdateメソッドにより新しいオブジェクトを作成して返しています。
        // 返した新しいオブジェクトは、以後stateメソッドで参照できます。
        // Containerは監視対象Storeが変更されたら当該Componentを再描画します。
        return state.update('message', () => 'Connected');

      default:
        return state;
    }
  }
}

export default new ConnectionStore();

ServerActions.js

import AppDispatcher from './AppDispatcher';
import ServerActionTypes from './ServerActionTypes';

const ServerActions = {
  connect() {
    // websocket周りは省略

    // JSXで指定されるアクションを実装します。
    // DispatcherにはStoreが登録されているので、識別子(type)を指定して引数(今回はws)を指定すれば良いです。
    AppDispatcher.dispatch({
      type: ServerActionTypes.CONNECTED,
      ws: ws,
    });
  }
};

export default ServerActions;

ServerActionSypes.js

const ServerActionTypes = {
  CONNECTED: 'CONNECTED',
};

export default ServerActionTypes;

まとめ

もっと複雑なインタラクションや、自動テストを考慮すると、今回の記事の内容では足りないかもしれません。いずれ実際のプロジェクトで使うときのため、引き続き勉強していこうと思います。

参考リンク

AWS Cloud Roadshow 2017 福岡