この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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;
まとめ
もっと複雑なインタラクションや、自動テストを考慮すると、今回の記事の内容では足りないかもしれません。いずれ実際のプロジェクトで使うときのため、引き続き勉強していこうと思います。