[初心者向け]Fluxを一周する
Fluxとは
Facebook社が提唱した、UI管理のためのアーキテクチャです。Webアプリケーションに限らず、様々なフロントエンド環境に適用できる考え方です。
画像出典 https://facebook.github.io/flux/docs/in-depth-overview.html#content
この記事ではFluxそのもの詳細には触れません。英語・日本語を問わず良質な記事がたくさんあるので、気になった方は検索してみてください。
今回は、ネット上にたくさんあるドキュメントやWebサイトを参考にして、Fluxを一通りやってみた内容になります。
重要なのは、常に以下の順序で動作し、例外は無いということです。
- Actionが発生する
- Storeの値が変更される
- 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;
まとめ
もっと複雑なインタラクションや、自動テストを考慮すると、今回の記事の内容では足りないかもしれません。いずれ実際のプロジェクトで使うときのため、引き続き勉強していこうと思います。