facebook/flux 2.1.0からFlux UtilsというStoreなどの実装が含まれるようになりました。

今回Flux Utilsを使って、指定したアカウントのはてなブックマークを検索するウェブアプリを書いてみました。

はてなブックマーク検索

これを作ってみてFlux Utilsについて思ったことを書いていきます。

Flux Utils

Flux Utilsの紹介ページに、Flux Utilsの解説が書かれています。

簡単にまとめると以下の4つのクラスがFlux Utilsとして提供されています。

  • Store
    • ベースとなるクラス
  • ReduceStore
    • Storeを継承
    • stateを保持していて、actionsに対してreduceすることでstate更新する
  • MapStore
  • Container
    • mixinsの代わり
    • React ComponentラップするComponentを作るコンテナ
    • Storeを登録しておいて、Storeの変更を元にComponentに通知する

Storeはおそらく直接使わない、MapStoreImmutable.jsに依存しているので、使うとしたらReduceStoreContainerがメインとなると思います。

azu/hatebu-mydata-searchでもReduceStoreContainerの2つを利用しました。

Flux UtilsはImmutable.jsを一部使っているのからも分かりますが(使わなくても問題ない)、Immutableなオブジェクトをstateとして使うのが前提となった作りになっています。

Immutable.jsはFlowやTypeScriptなどの型付き言語で使いやすくなってたり、Immutableな実装でのパフォーマンスにおけるメリットはありますが、癖があって普通に扱うのが難しいのです。

そのため、今回はImmutableなstateオブジェクトとして、もう少し扱いやすいchristianalfoni/immutable-storeを利用しました。

追記: immutable-storeはDEPRECATEDなので、omniscientjs/immstructimmutable-jsを使いましょう

Flux Utilsを利用する場合、以下のようにflux/utilsとパスを指定して読み込むことで利用できます。 (Facebookはこういうのが多いですが、browserifyした時とかに使ってないものが勝手に含まれないから?)

import {ReduceStore} from 'flux/utils';

余談:ファイルサイズ

Flux Utilsを含む前後のfacebook/fluxのファイルサイズは以下のような感じです。

Before:

before

After:

after

gif

facebook/fluxdistにはデフォルトでFlux Utilsが含まれていないため、Flux Utilsを明示的にimportしない限りファイルサイズは増えたりはしないと思います。

また、immutable.jsに直接依存してるのはMapStoreだけで、他のReduceStoreなどはimportしてもimmutable.js含まれないため、そこまでファイルサイズ気にしなくても良いと思います。

MapStoreを使う場合はImmutable.jsを使うことになるので、結構大きくなります。

import {MapStore} from 'flux/utils';

Flux + Reactなアプリ作成の流れ

ここからはazu/hatebu-mydata-searchがどういう順番で作成したかに沿った流れで書いていきます。

最初はビルド環境やindex.htmlを置いてbuild.jsを読み込めるようにします。

babel+babelify+browserifyな感じでJavaScriptを書けるようにしてます。 CSSはcssnextPureを使いました。

ステートレスコンポーネント

コミットをそこまでキレイに分けてないので上記のコミットに含まれてますが、まずはステートレスなReact Componentを作成していくようにしています。 (React Componentでthis.stateを使わないと考えれば書きやすいと思います)

具体的には以下のユーザ名の入力するinput要素をInputUserName.jsという名前で最初に作っています。

input

import React from "react"
export default class InputUserName extends React.Component {
    onSubmit(event) {
        event.preventDefault();
        var name = React.findDOMNode(this.refs.userName).value;
        this.props.onSubmit({
            name
        });
    }

    render() {
        return <div className="InputUserName">
            <form className="InputUserName-form pure-form" onSubmit={this.onSubmit.bind(this)}>
                <input type="text" ref="userName"></input>
                <input className="pure-button" type="submit" value="変更"></input>
            </form>
        </div>
    }
}

この作ったComponentを確認する意味も含めて、App.jsというエントリポイントとなるファイルを作って、単純に読み込んでdocument.bodyへ表示しています。

import React from "react"
import InputUserName from "./components/InputUserName"
function onSubmit({name}) {
    console.log(name);
}
React.render(<InputUserName onSubmit={onSubmit}/>, document.body);

こうすることでsubmitイベントがちゃんと動いてるのかをstateがなくても確認できたりするので、HTMLでまず構造を書いてみるのに近い作業で入り口とするのに向いています。

同じ要領でブックマークのデータを表示するList要素としてBookmarkListを作って

import React from "react"
export class BookmarkListItem extends React.Component {
    render() {
        var {title, url, comment} = this.props.bookmark;
        return <li className="BookmarkListItem">
            <a href={url}>{title}</a>

            <p>{comment}</p>
        </li>
    }
}
export default class BookmarkList extends React.Component {
    render() {
        var items = this.props.bookmarks.map(bookmark => {
            return <BookmarkListItem key={bookmark.url} bookmark={bookmark}/>;
        });
        return <ul className="BookmarkList">
            {items}
        </ul>
    }
}

App.jsにダミーの配列データ(bookmarks)を置いて表示しています。


+import BookmarkList from "./components/BookmarkList"
 function onSubmit({name}) {
     console.log(name);
 }
-React.render(<InputUserName onSubmit={onSubmit}/>, document.body);
+var bookmarks = [
+    {
+        title: "タイトル",
+        url: "http://localhost:3000/",
+        comment: "[test] メッセージ"
+    },
+    {
+        title: "NW.jsでのバイナリリリース",
+        url: "https://efcl.info/2014/09/05/node-webkit-binary-release/",
+        comment: "[test] ビルドが楽になりたい"
+    }
+];
+React.render(<div>
+    <InputUserName onSubmit={onSubmit}/>
+    <BookmarkList bookmarks={bookmarks}/>
+</div>, document.body);

後は同じようにブックマークをフィルターするキーワードを入れるSearchBoxを作ったりしています。

ここまでで、画面に必要な要素をステートレスコンポーネントとして用意してとりあえず表示できるようになりました。

もちろんまだCSSが存在してないので、デザインとか配置については何も設定されていないですが、上記のコンポーネントを見て分かるようにInputUserNameというコンポーネントには.InputUserNameというCSSクラスが適応されています。

これはSUIT CSSという命名ルールと殆ど同じで、MyComponentというコンポーネントには.MyComponentというクラス名をつけるという命名ルールです。

MyComponent以下にある要素のクラスなら、.MyComponent-partという感じでReact Componentみたいなものとは相性がいいと思うのでよく使ってます。

Flux Utilsの導入

コンポーネントが揃ったらボタンを押したらデータを読み込むみたいな動きをつけていくので、ここでやっとFluxを導入します。

このコミットだとImmutable.jsを使ってますが、最終的にはimmutable-storeを使っています。(値を取得するときにget("key")みたいな事をしなくて良いので自然に使える)

Fluxをどこから実装するかですが、Flux Utilsを使った場合はStoreかActionCreatorあたりからが自然になると思います。 (ContainerはStoreがないとそもそも始まらない)

ActionCreator

自分はActionにkeysを置きたいので、まずはActionCreatorから書きました。 (一般的にはconstantsみたいな別のファイルにtypeだけを定義したりする)

ただ、Flux Utilsのサンプルを見てみると分かるようにそもそもActionCreatorがなくなっていて、FlowTypeで書かれたActionの型だけが定義されています。

自分の中ではそれぞれ以下のような認識で単語を使っていますが、正直あんまり深く考えてないです。

  • Action: ペイロードオブジェクト(typeとデータを持ってる)
  • ActionCreator: ActionをDispatcherに渡してdispatchする関数をまとめたヘルパークラス

Flux: Actions and the Dispatcher | React

SearchActionというクラスは上記の定義だとActionCreatorに当たるものですが長いので。。

簡単に書くと以下のように特定のtypeを持ったActionオブジェクトをdispatchするだけのメソッドを持ったクラスです。

import {getMyData} from "./SearchUtils"
import Dispatcher from "../Dispatcher";
export var keys = {
    reset: Symbol("reset"),
    inputText: Symbol("inputText"),
    loadItems: Symbol("loadItems")

};
export default class SearchAction {
    static reset() {
        Dispatcher.dispatch({
            type: keys.reset
        });
    }

    static inputText(text) {
        Dispatcher.dispatch({
            type: keys.inputText,
            text
        });
    }

    static loadItems(userName, fromDate) {
        return getMyData(userName, fromDate).then(items => {
            Dispatcher.dispatch({
                type: keys.loadItems,
                items
            });
        });
    }
}

ここに出てくるDispatcher.jsはシングルトンなDispatcherで、Flux Utils的にもDispatcherはシングルトンを前提としたものとなっているようです。

Store

次にReduceStoreを使ってフィルターとなる単語と表示してるブックマークのstateを管理するStoreを作ります。

ReduceStoreはsetStateのようなメソッドは持っていません。 代わりreduce(state, action)というメソッドを実装して、ここでreturnした値が次のstateとなるような仕組みになっています。

Actionのkeysを使って、keys.loadItemsというtypeのActionが来たならば(ここでいうならSearchAction.loadItems()が呼ばれたならば)、現在のstateのitemsをaction.itemsに置き換えたものを新しいstateにするといった感じです。

import { ReduceStore } from 'flux/utils';
import SearchDispatcher from "./SearchDispatcher";
import { keys } from "./SearchAction"
import Immutable from "immutable-store"

class SearchStore extends ReduceStore {
    getInitialState() {
        return Immutable({
            "text": "",
            "items": []
        });
    }

    reduce(state, action) {
        switch (action.type) {
            case keys.inputText:
                return state.set("text", text);
            case keys.loadItems:
                return state.set("items", action.items);
            default:
                return state;
        }
    }
}
// ここもシングルトン
// Export a singleton instance of the store, could do this some other way if
// you want to avoid singletons.
// と書いてあるぐらいなので
const instance = new SearchStore(SearchDispatcher);
export default instance;

Container

Flux Utilsで書いてて結構気に入ってるのはContainerという仕組みです。

これはstatic getStores()で並べたStoreから”Change”イベントがemitされたら、static calculateState(prevState)でReact Componentのstateとなるものを返して、React ComponentにsetState(calculateState()))されるというイメージです。

Containerで包むべきReact ComponentはRootとなるComponentとするべきです。 これはReactを書くときのパターンとしてよくある、stateを中央の一箇所に集めるパターン(Centralize State)で書いているとわかりやすく適応できます。 (子となるReact Component内でstateを持つこともありますが、そのstateは外から不要なstateであるべきです)

最初にステートレスコンポーネントで書いていたのも、App.jsからstateを渡すことで、それぞれ子となるコンポーネントがstateを持たなくてもよくなるからです。

 import InputUserName from "./components/InputUserName"
 import BookmarkList from "./components/BookmarkList"
 import SearchBox from "./components/SearchBox"
+import SearchStore from "./Search/SearchStore"
+import SearchAction from "./Search/SearchAction"
+import {Container} from 'flux/utils';
 function onSubmit({name}) {
     console.log(name);
 }
 function onChange(text) {
+    SearchAction.inputText(text);
 }
 var bookmarks = [
@@ -22,8 +26,27 @@ var bookmarks = [
         comment: "[test] ビルドが楽になりたい"
     }
 ];
-React.render(<div>
-    <InputUserName onSubmit={onSubmit}/>
-    <SearchBox onChange={onChange}/>
-    <BookmarkList bookmarks={bookmarks}/>
-</div>, document.body);
+export default class App extends React.Component {
+    static getStores() { // 変更を監視したいStore一覧
+        return [SearchStore];
+    }
+    // returnされたものが`setState`される
+    static calculateState(prevState) {
+        return {
+            search: SearchStore.getState()
+        }
+    }
+
+    render() {
+        return <div>
+            <InputUserName onSubmit={onSubmit}/>
+            <SearchBox onChange={onChange}/>
+            <BookmarkList bookmarks={this.state.search.get("items")}/>
+        </div>
+    }
+}
+
+SearchAction.loadItems();
// Container.createで`<App />`をラップしてる
+const AppContainer = Container.create(App);
+React.render(<AppContainer />, document.body);

これにより、ContainerがStoreの変更を監視してViewに変更を通知できるようになります。

flux

ここで問題になるのが、Storeの変更とは何かです。

Flux Utilsでは他のFlux実装のようにsetStateeventEmitter.emit("change")のような部分は隠蔽されています。

Flux UtilsのReduceStoreにおけるStoreの変更とはreduce(state, action)の結果が現在とは異なるstateオブジェクトである時をいいます。

現在とは異なるstateオブジェクトになった ==> stateが変更された ==> Storeの変更イベントが発火

ということになっています。

異なるstateオブジェクトかどうかという判定はareEqual(one: TState, two: TState): booleanというメソッドで行われていて、以下のような判定となっています。

  areEqual(one: TState, two: TState): boolean {
    return one === two;
  }

StateがImmutable?

ここで最初の方にも書いてたFlux UtilsがなぜImmutable前提と言えるのかという話をしておきます。

先ほどのareEqual===でstateを比較しているので、以下のようなstateオブジェクトのプロパティを変更した場合でも、同じstateとみなされます。

var state = { key : "value" };
state.key = "!!!";
var newState = state;
areEqual(state, newState);// true

そのため、Storeのstateを変更したという状態にするにはreduceで現在のstateオブジェクトをcloneしてから変更するなどが必要になります。

そのため、Flux UtilsではImmutable.jsimmutable-storeといったものを使ってstateを扱わないとかなり面倒になると思います。

immutable-storeだと以下のように===で異なるオブジェクトが作れます。

var state = Store{
  foo: 'bar'
};

var newState = state.set('foo', 'bar2');
areEqual(state, newState);// false

このstateはImmutableであるというのは、stateが汚れたりしないのでいいところもありますが、逆にsetStateのような無理やりstateを変えたいという時に結構面倒な事があります。

ローカルストレージからの復元

azu/hatebu-mydata-searchでも一つ遭遇して、このアプリでは取得済みのブックマークやユーザー名などはIndexedDBに保存しています。

画面を表示した後、非同期で取得したデータをstateを設定し直すのにsetStateみたいな単純な方法がないので以下のような作りにしました。

restoreTypeというtypeを追加して、ストレージから取得したオブジェクトをstateにするというものをreduceに追加しています。

要はActionCreatorからrestoreTypeというActionとストレージにあるstateをdispatchするという、他のActionと全く同じ流れを踏むようになっています。

import {ReduceStore} from 'flux/utils';
import Dispatcher from "../Dispatcher";
import { keys } from "./HatebuAction"
import Immutable from "immutable-store"
import {getStorage, setStorage} from "../LocalStorageContainer"
const restoreType = Symbol("restore");
class HatebuStore extends ReduceStore {
    constructor(...args) {
        super(...args);
        getStorage(this.constructor.name).then(state => {
            Dispatcher.dispatch({
                type: restoreType,
                state
            });
        });
        this.addListener(() => {
            var storeName = this.constructor.name;
            var state = this.getState();
            setStorage(storeName, state).catch(error => {
                console.warn(error, " on " + storeName);
            });
        });
    }

    getInitialState() {
        return Immutable({
            "userName": ""
        });
    }

    reduce(state, action) {
        switch (action.type) {
            case keys.inputUser:
                return state.set("userName", action.userName);
            case restoreType:
                return state.import(action.state);
            default:
                return state;
        }
    }
}

最初はReduceStoreをハックしてどうにかしようとしていますが結構難しい事がわかったので、restoreTypeというActionでやる形にしています。

やってみるとFlux Utilsはこういった感じで結構ルール外なことはやりにくいようになっている気がするので、ギブス的にもなってるような気がします。

ストレージから復元もActionでやったほうが例外的なルールもなくなるので真っ当な設計だと思います。

まとめ

flux-utilsについてでも書いていましたが、特にMapStoreなどはFlowやTypeScriptといった型付き言語だと使いやすいような形となっています。 そういったものと一緒にFlux Utilsを使うといろんな人がいても書き方がかなり統一されるような感じがします。

またFlux Utilsのページの最初にも書いてあるように、別にFluxアーキテクチャをやる際にFlux Utilsを絶対使うべきというものでもないと思います。

Flux Utils is a set of basic utility classes to help get you started with Flux. These base classes are a solid foundation for a simple Flux application, but they are not a feature-complete framework that will handle all use cases. There are many other great Flux frameworks out there if these utilities do not fulfill your needs.

Flowで書きやすいようにするというモチベーションがありそうですが、immutable state、pure functionといったあたりはrackt/reduxなどにも近いような話も出てきますが、その前に一度Flux Utilsを触ってみると面白いかもしれません。

以前のDispatcherしかなかったfluxモジュールに比べて、コード量も少なくなり、Storeなどの形も一定になって見通しが良くなったり、Containerが結構いい感じなので普通に使えるといった印象です。