Panda Noir

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

Riot.js+Redux+Immutable.jsでカレンダーアプリを作った

なんでReactじゃないのかって?Riotの方が好きだからですよ。

つくったもの

Simple Calendar

シンプルなカレンダーです。

(注: この記事は「カレンダーの作り方」、ではなく「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>