Panda Noir

JavaScript の限界を究めるブログです。

Fluxとはなんぞや

github.com

ここのFlux conceptsを参考にFluxについて解説したいと思います。

Fluxのパーツ

Fluxには4つのパーツがあります。

  • Dispatcher
  • Store
  • Action
  • View

Dispatcher

Dispatcherは、Actionを受け取り、Storeに伝え(Dispatch)ます。オブザーバはDispatcherの一種です。

Dispatcherは1つのアプリケーション中に2つ以上作ってはいけません

RiotControlはDispatcherのみを提供します。

RiotControl.addStore(store); // addStoreで登録されたStoreには自動でActionが伝えられる
RiotControl.trigger('create-user'); // すべてのStoreにActionが伝わる

Store

Storeはアプリケーションのデータを保持します。StoreはActionを受け取るため、Observableであるべきです。StoreはAction以外で変更されるべきではありません。Storeは変更されたら、changeイベントを発行する必要があります。

Action

Actionは内部APIを示すデータ構造です。

const deleteAction = {
    type: 'delete-todo',
    todoID: '1234'
};
const changeAction = {
    type: 'store-changed'
};

といっても、実際の実装ではActionは

dispatcher.trigger(action.type, action.data);

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

View

Viewはビューです。Storeのデータを表示することができます。ただし、Storeのデータを扱う場合、Storeをsubscribeしなければなりません(Storeはchangeイベントを発行するのでsubscribeするのは簡単です)。

処理の流れ

View側でなんらかのイベント(クリックイベントやキーダウンイベント)が起きます。Dispatcherがそれを受け取り、全てのStoreへそのことを伝えます。Storeは必要があれば変更を行い、changedイベントを発行します。Viewはchangedイベントを受け取ったら変更を反映します。

外部との通信を行う場合は、通信を投げて、結果をDispatcherが受け取り、Storeへ伝え、Viewがchangedイベントを受け取り、変更を反映します。

簡単な例

簡単なカレンダーアプリを題材にFluxについて考えてみます。

カレンダーアプリはヘッダ部分、ボディー部分、ディティール部分にわかれます。

ヘッダ部分は「カレンダーの現在の年、月の表示」、「次の月へ、前の月へ進めるボタン」を持ちます。ボディー部分はカレンダーを表示します。ディティール部分は、ボディー部分で選択された日についての情報を表示します。

Store

Storeには「現在の年、月」「選択された日」の情報を持たせればいいとわかります。

Action

「次の月へ」ボタンが押されたら"next-month"Actionを発行します。日が選択されたら、"select-date"Actionを発行します。このActionは選択された日のデータも持ちます。

Dispatcher

Dispatcherは適当でOKです。なんなら自前でつくることすらできます。

class Dispatcher {
    constructor() {
        this.stores = [];
    }
    addStore(store) {
        this.stores.push(store);
    }
    trigger(event, action) {
        for (const store of this.stores) {
            store.trigger(event, action);
        }
    }
}

全体像

const today = new Date();
const dispatcher = new Dispatcher();
const calendarStore = {year: today.getFullYear(), month: today.getMonth(), events: {}, actionTypes: {changed: 'calendar-store-changed'}};
calendarStore.trigger = function(event, data) {
    for (const event of this.events[event]) {
        event(data);
    }
};
calendarStore.on = function(event, listener) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(listener);
};
// riot.observable(calendarStore); // Riotを使うならこれでOK
calendarStore.on('next-month', () => {
    calendarStore.month++;
    if (calendarStore.month == 12) {
        calendarStore.month = 0;
        calendarStore.year++;
    }
    dispatcher.trigger(calendarStore.actionTypes.changed);
});
calendarStore.on('prev-month', () => {
    calendarStore.month--;
    if (calendarStore.month < 0) {
        calendarStore.month = 11;
        calendarStore.year--;
    }
    dispatcher.trigger(calendarStore.actionTypes.changed);
});
dispatcher.addStore(calendarStore);
$('#next-month').on('click', () => dispatcher.trigger('next-month'));
$('#prev-month').on('click', () => dispatcher.trigger('prev-month'));
$('.date').on('click', function() {dispatcher.trigger('changed', {date: $(this).text()});});

(コードを書くのが面倒だったのでjQueryを使用しています)

ViewはcalendarStoreのchangedを監視して適当に処理すればOKです。ということで、 ほぼこれだけのコードでカレンダーアプリが書けます。直感的で素敵ですね。