ここの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です。ということで、 ほぼこれだけのコードでカレンダーアプリが書けます。直感的で素敵ですね。