なんでReactじゃないのかって?Riotの方が好きだからですよ。
つくったもの
シンプルなカレンダーです。
(注: この記事は「カレンダーの作り方」、ではなく「Riot.js+Redux+Immutable.jsで開発する際のもろもろについて」です。カレンダーのソースコードはリンク先のHTMLをご覧ください)
環境
- Riot.js: 3.0.1
- Immutable.js: 3.8.1
- Redux: 3.6.0
概要
StoreをImmutable.js+Reduxで作り、ViewはRiot.js、ActionやActionCreatorは自前で作りました。
StateをImmutableにする意味
Reduxでは、一貫してStateは変わりますが、Stateが指すもの自身は変更されません。
ReduxにおいてStateはActionが起きたときのみ変更されます。また、ReducerがStateとActionを受け取り、Stateを返し、それが次のStateとなります。ここで重要なことは、Reducer内でState自身は変更されないということです。
ということはStateはImmutableであっても問題ありません。むしろ、下手に外部から変更可能にしておくよりも安全になります。
const today = new Date(); const calendarStore = createStore((state = Immutable.Map({year: today.getFullYear(), month: today.getMonth()}), action)=> { // この関数内で、state自身は変更されていない if (action.type === NEXT_MONTH) { if (state.get('month') == 11) return state.set('year', state.get('year') + 1).set('month', 0); return state.set('month', state.get('month') + 1); } if (action.type === PREV_MONTH) { if (state.get('month') == 0) return state.set('year', state.get('year') - 1).set('month', 11); return state.set('month', state.get('month') - 1); } return state; });
実にきれいに書けています。
ActionとActionCreatorの実装
Fluxのコンセプトどおり、Actionをデータ構造として定義しています。ActionはActionCreatorが呼び出されることで生成されます。
// action.js // Actionのタイプ export const NEXT_MONTH = 'next-month'; export const PREV_MONTH = 'prev-month'; // ActionCreator export const nextMonth = ()=> ({type: NEXT_MONTH}); export const prevMonth = ()=> ({type: PREV_MONTH});
この例ではActionは定数となっているのでActionCreatorの必要性をあまり感じません。しかし、例えばクリックされた位置情報を渡したいときなどにActionCreatorを使うと、動的にActionを生成できます。
const click = (x, y) => ({ type: CLICK, payload: { x, y } }); // ActionCreatorが必要な例
(payloadというのは、Flux Standard Actionを参考にしてます)
Dispatcherはどうしたのか
storeに直接Actionを伝播(dispatch)する形となります。「Storeが複数あったらどうするんだ」という声が聞こえてきそうですね。安心してください。combineReducers()という関数を使うことで複数のReducerを一つにまとめることができます。というかRedux的にはまとめなければなりません。
const reducer1 = (store, action)=> store; const reducer2 = (store, action)=> store; const store = createStore(combineReducers({ reducer1: reducer1, reducer2: reducer2 }));
これに対してdispatchを行うようにすることで、Dispatcherなしでも成立します。
ただし、reducer1、reducer2の実装は特に変更することはありませんが、 store.getState().reducer1
として取り出さなければいけなくなり、若干まどろっこしくなります。
Riot.jsとの連携
このような感じとなります。
const INCREMENT_COUNTER = 'increment-counter'; const RESET_COUNTER = 'reset-counter'; const incrementCounter = ()=> ({type: INCREMENT_COUNTER}); const resetCounter = ()=> ({type: RESET_COUNTER}); const store = createStore((state = 0, action)=> { if (action.type === INCREMENT_COUNTER) { return state + 1; } if (action.type === RESET_COUNTER) { return 0; } return state; });
<component> {counter} <button onclick={clicked}>click me</button> <button onclick={reset}>reset</button> <script> clicked() { store.dispatch(incrementCounter()); } reset() { store.dispatch(resetCounter()); } this.counter = store.getState().counter; store.subscribe(()=> { this.counter = store.getState().counter; this.update(); }); </script> </component>