reactjs
redux

react+reduxで非同期処理を含むtodoアプリを作ってみる

More than 3 years have passed since last update.

夏休みの自由研究と言うことでreact+reduxを触ってみたのでその記録。

成果物

まだ全然完成してないけど、とりあえずここにコードは置いてみた。
https://github.com/hokuma/react-redux-async-sample

  • 一覧表示(動く)
  • 追加フォーム
  • ぐるぐる出す

辺りまではやりきりたい。

フロント技術の進化そのものにキャッチアップしながらのためいろいろ微妙なコードはあるけれど、追々きれいに(なったらいいなー)。

使ったもの(主だったもの)

React

http://facebook.github.io/react/
http://qiita.com/advent-calendar/2014/reactjs

あまり深く説明しませんが、個人的には以下の特徴がある(そういう実装に自然となる)と理解しています。

  • UIに関することのみ
  • データの流れは常に一方向(親コンポーネントから子コンポーネントへ)
  • Virtual DOMを使うことで差分描画みたいなめんどくさいことを考えずに済む

データの流れが一方向、というのが個人的には好き(双方向ではない)で、描画処理の流れが追いやすく、UI周りのコードのスパゲティ化が防げそうな感じがします。双方向bindingしない分コードが増える疑惑はありますが、最終的にはメンテしやすくなるのでは、と思っています。

Redux

Flux

ReactはあくまでViewなので、アプリ実装に必要となるデータの管理や、ユーザアクションに対するハンドリングなどは自分で実装しなければなりません。

Facebookはそのためのアーキテクチャとして
Flux: http://facebook.github.io/flux/
というものを提唱しています。

私の理解としては、全てのデータの流れを一方向に、です。
上のページを見に行くと
Action -> Dispatcher -> Store -> View
という図がありますが、Fluxアーキテクチャではデータフローは常にこの方向です。Viewから直接Storeを更新することは許されません。

Viewにおいて、クリックやチェックボックスをOnにするなどアプリの状態に変化が起きると、

  • Actionが起こり、
  • そのアクションから適切な処理をDispatchが選択し、
  • その処理内でStoreが更新され(ドメインロジック)、
  • 更新されたStoreの状態がViewに反映される

といった処理が順次行われて行きます。

上記のような構造を採用することで、ActionやDispatcherなど各登場人物の役割をシンプルにしつつ、データの流れを追いやすくし、アプリケーションをメンテしやすくしよう、ということだと思います。

Redux

あくまでFluxは実装ではなくアーキテクチャです。そのため、Fluxアーキテクチャの各要素は自分で実装しないといけません。とはいえ、自分でゼロから実装する必要はなく、世の中にはFluxを実装したフレームワークが各種あり、

https://github.com/voronianski/flux-comparison

を見る限り群雄割拠感あります。

この中で今回触ってみたのはReduxです(ReduxをFluxと言うかは作者自身がYesでもありNoでもあるみたいなことを言ってますが)。

理由としては、flummoxやmarty.jsなどの他のライブラリが公式でオススメしてるからです。うちはもう更新止めるからRedux使ってみたらいいよ!的なことがgithubのトップに書いてあります。

ReduxはFluxインスパイアという感じです。Fluxにあったような一方向データフローなどは採用しつつも、Dispatcherは出てきません。逆にFluxにはないがReduxにはあるものがあり、Reducerというものがあります。Reducerは起きたアクションと現在のアプリ状態から新しいアプリ状態を決定する役割を担っていて、純粋に関数であること、という特徴があります。従って、Reducerの中ではAPIコールなどの関数の引数以外に依存するような処理はやってはいけません。でもAPIコールとか必須でしょ、となるわけですが、その辺もActionとReducerの枠組みでどうにか実装せい、というのがReduxで、Fluxにはなかったポリシーです。

実装

非同期処理のとこだけ解説。import文とかは省略するし、Reduxの細かい解説も割愛。その辺はReduxのreadme読んで。

Action

export const FETCH_TODOS = 'FETCH_TODOS';
export function fetchTodos() {
    return {
        type: FETCH_TODOS,
    };
}

export const RECEIVE_TODOS = 'RECEIVE_TODOS';
export function receiveTodos(todos) {
    return {
        type: RECEIVE_TODOS,
        todos: todos
    };
}

function getTodos(store) {
   return dispatch => {
       dispatch(fetchTodos());
       return $.getJSON('http://127.0.0.1:3000/todos')
         .then(data => dispatch(receiveTodos(data.todos)));
   };
}

export function getTodosIfNeeded() {
    return (dispatch, getState) => {
        if(getState().isFetching) {
            return Promise.resolve();
        } else {
            return dispatch(getTodos());
        }
    };
}

まず、Actionを定義します。肝心なのは、一見すると一つのアクションにまとめられそうな「データを取ってくる」処理を、「データを取りにいく」、「データを受信した」という二つのアクションに分けるところです。

次に、それらのアクションを非同期で呼ぶための関数を返すgetTodosIfNeededを用意します。既に取得中であれば何もせず、そうでなければまずFETCH_TODOSアクションを起こし、その後データ取得後にRECEIVE_TODOSアクションを起こしています。

Reducer

function todoList(state = {
    isFetching: false,
    todos: []
}, action) {
    switch(action.type) {
    case FETCH_TODOS:
        return assign({}, state, {isFetching: true});
    case RECEIVE_TODOS:
        return assign({}, state, {isFetching: false, todos: action.todos});
    default:
        return state;
    }
}

const rootReducer = combineReducers({
    todoList
});

export default rootReducer;

RECEIVE_TODOSの処理の中で非同期処理でデータを取得するのではなく、既に取得した結果を引数でもらい、状態を更新しています。Reducerはあくまで関数なので、この中でAPIコールなどはせず、取得した結果をもらうだけです。

FETCH_TODOSはデータを取得しにいくアクションなので、データ取得中のisFetchingだけ更新しています。

こういう処理は非同期処理のコールバックの中でもできますが、「○○中」みたいな状態も全てアプリの状態の一つとして管理して、変更するためのアクションを定義します。

呼び出し

const createStoreWithMiddleware = applyMiddleware(
    thunkMiddleware
)(createStore);

const store = createStoreWithMiddleware(rootReducer);
store.dispatch(getTodosIfNeeded());

dispatchにはアクションを定義したオブジェクトを渡すのですが、redux-thunkというmiddlewareを使うことで、非同期処理でアクションを起こすような関数をdispatchに渡せるようになります。この辺はそういうものと理解しておいてください(というか自分もまだそんな感じの理解)

これで、Reducer自体は関数のまま、同期/非同期処理のどちらも同じインタフェース(store.dispatch)を使って扱えるようになりました。

まとめ

非同期関数の呼び出しを「呼び出し始める」「データを受信する」といった形で状態遷移ごとにアクションに分解することが鍵。一見すると実装増えそうだけど、ローディング中にしつつデータを取りに行く、みたいな処理をそれぞれのアクションに分けて実装することで、処理も追いやすくなるし一つ一つの実装もシンプルに保ちやすいのではと感じました。