Reduxのmiddlewareを積極的に使っていく

最初はちょっととっつきにくいけど、責務がはっきり分かれていて比較的コードがごちゃごちゃになりにくい（と思っている）Reduxですが、やはり実戦投入するとどうにも扱いにくい部分が出てきます。

特にそう感じるのは通信系の処理、ユーザーとのインタラクションです。これってつまるところ非同期処理なんですが、処理を依頼する側、つまりActionを投げる側としては「あとのことはまかせた！」と言いたい。Actionを投げる部分ってのはだいたい何かのイベントハンドラだったりしますが、そういう場所に通信やインタラクションの処理をダラダラと書きたくない。

本稿ではそれらの面倒な部分を責務が分離されたメンテナンスのしやすいコードになるようにmiddlewareを活用する例をいくつか紹介します。

## その前にmiddlewareについて

Reduxの[公式ドキュメント](http://redux.js.org/docs/advanced/Middleware.html)ではログ出力を例に取り、アプリケーション本体から分離して段階的に設計を洗練させることで、middlewareによって何が解決されて、内部的にどのような仕組みで動いているのか理解することができます。一読の価値があります。

## ActionをDispatchする前にブラウザの確認ダイアログを表示する

### 問題意識と具体例

入力内容をリセットするボタンのついたウェブフォームを想定して下さい。よくある要件としてリセットボタンを押すと確認ダイアログが表示されて「はい」のときのみフォームがリセットされます。

ひとまずブラウザの確認ダイアログ `window.confirm(...)` を使用しますが、いずれBootstrapのモーダルダイアログに切り替えることになるかもしれません。そうなるとコード全体に散らばったコード片を書き換えるのはなかなか骨が折れます。それにフォームをリセットするActionを送信するコードの大半にとっては、確認ダイアログを表示すべきかどうか考えたくありません。**つまり投げっぱなしにしたいわけです。**

### Before

Fluxでは何をするにもActionが起点になるので、リセットボタンが押されたら該当するAction、ここでは `RESET_FORM` がStoreに送られてReducerによってリセット後の新しい状態が生成されます。大まかに書くと以下のようなコードです。

```actions.js
import { RESET_FORM } from '...';

export function resetForm() {
  return { type: RESET_FORM };
}
```

```reducers/form.js
import { RESET_FORM } from '...';

const handlers = {
  [RESET_FORM](state, action) {
    return initial;
  }
};

const initial = {
  name: '',
  email: ''
};

export default function formReducer(state = initial, action) {
  const handler = handlers[action.type];
  return handler ? handler(state, action) : state;
}
```

```components/app.js
import { resetForm } from '...';

class App extends React.Component {
  // ...
  // リセットボタンが押されたら呼び出されるハンドラ
  handleReset() {
    if (window.confirm('本当にリセットしてもいいですか？')) {
      // 「はい」のときだけActionを生成してStoreに送信する
      this.props.dispatch(resetForm());
    }
  }
  // ...
}
```

というわけで確認ダイアログ表示と、結果に応じてActionを送信する部分をmiddlewareとして切り出します。

### Middleware

```middlewares/confirm.js
export default const confirmMiddleware = store => next => action => {
  if (!action.meta || !action.meta.confirm) {
    return next(action);
  }

  if (window.confirm(action.meta.confirm)) {
    delete action['meta']['confirm'];
    return next(action);
  }
};
```

`confirmMiddleware` を組み込みます。

```store.js
import { reducer } from '...';

const store = applyMiddleware(
  thunk, confirm
)(createStore)(reducer);
```

### After

先ほどのコードは次のように変わります。

```actions.js
import { RESET_FORM } from '...';

export function resetForm() {
  return {
    type: RESET_FORM,
    meta: { confirm: '本当にリセットしてもいいですか？' }
  };
}
```

```components/app.js
import { resetForm } from '...';

class App extends React.Component {
  // ...
  // リセットボタンが押されたら呼び出されるハンドラ
  handleReset() {
    // 「はい」が選択されたら送信される
    this.props.dispatch(resetForm());
  }
  // ...
}
```

## API呼び出しをチェインさせる

### 問題意識と具体例

APIサーバとの通信処理において、あるデータを取得したらその結果を元にさらに追加のデータを読み込んで、ということがよくあります。・・・本来よくあってはならないことですが、現実そういったAPIを相手にしないといけないことはあります。もしくはログイン処理後に何かデータを読み込む、といった「あるAPIを呼び出したらその次に別のAPIを呼び出したい」状況です。

**APIチェインをやろうとしたとき、どういう実装にするのが良いのでしょうか？**

いくつか考えられます。API呼び出し完了のActionを処理するReducerで追加のデータをリクエストするActionを送信する方法。rootコンポーネントの `componentWillUpdate` などで最初のAPI呼び出しによるデータ変更を検出して、それを元に追加のデータをリクエストするActionを送信する方法。後述の `redux-api-middleware` を使った場合、成功時に呼び出されるコールバックに追加のデータをリクエストするActionを送信するコードを書くこともできます。しかしどれもしっくりきません。

### Before

しっくりこないんですが、例示するコードはComponentで変更を検出して追加のデータをリクエストする方法にしてみました。

さて、ReduxにおけるAPI呼び出しにもmiddlewareを使うとコードの見通しが良くなります。[`redux-api-middleware`](https://github.com/agraboso/redux-api-middleware) はまさにそのためのmiddlewareです。プロジェクトごとにAPI呼び出しのActionがバラバラになるのを防ぐために導入しています。

ちょっと解説すると `redux-api-middleware` は `CALL_API` という Symbol をキーに持ったActionを検出すると設定にもとづいてリクエストを送信します。`types` にはAPI呼び出しのライフサイクルにおける各段階に対応したActionを「リクエスト送信」「成功」「失敗」の順に指定します。単純に文字列を渡すとAction Typeとして扱われてActionが送信されます。この場合Actionオブジェクトは`type`指定のみになるため、リクエストのレスポンスをActionに含めたい場合は文字列の代わりに、送信して欲しいActionオブジェクトを渡します。`payload` が関数の場合は呼び出した結果が `payload` としてセットされます。

```actions.js
export function login(payload) {
  return {
    [CALL_API]: {
      endpoint: '/api/users/login',
      method: 'POST',
      body: payload,
      types: [REQUEST_LOGIN, {
        type: SUCCESS_LOGIN,
        payload: (action, state, res) => {
          return res.json().then(payload => payload);
        }
      }, FAILURE_LOGIN],
    }
  };
}

export function fetchPost(id) {
  return {
    [CALL_API]: {
      endpoint: `/api/posts/${id}`,
      method: 'GET',
      types: [REQUEST_FETCH_POST, SUCCESS_FETCH_POST, FAILURE_FETCH_POST]
    }
  };
}
```

```components/app.js
import { login, fetchPost } from '...';

class App extends React.Component {
  // ...
  componentWillUpdate(nextProps) {
    if (this.props.login === false && nextProps.login === true) {
      // ログイン後に記事を読み込む
      nextProps.posts.forEach(id => {
        this.props.dispatch(fetchPost(id));
      });
    }
  }
  // ...
  // ログインボタンのイベントハンドラ
  handleSubmit(username, password) {
    this.props.dispatch(login({ username, password }));
  }
  // ...
}
```

それではログイン後の記事読み込みの部分をmiddlewareを使って分離します。

### Middleware

```middlewares/chain.js
import { fetchPost } from '...';

const hooks = {
  [SUCCESS_LOGIN](store, action) {
    // ログイン処理のレスポンスから記事のIDリストを取得
    const ids = getPostIds(action.payload);
    ids.forEach(id => {
      store.dispatch(fetchPost(id));
    });
  }
};

export default const chainMiddleware = store => next => action => {
  const hook = hooks[action.type];
  hook && hook(store, action);
  return next(action);
};
```

`chainMiddleware` を組み込みます。

```store.js
import { apiMiddleware as api } from 'redux-api-middleware';
import chain from '...';
import { reducer } from '...';

const store = applyMiddleware(
  thunk, api, chain
)(createStore)(reducer);
```

### After

状態変更の検出部分がまるごと消えました。

```components/app.js
import { login } from '...';

class App extends React.Component {
  // ...
  // ログインボタンのイベントハンドラ
  handleSubmit(username, password) {
    this.props.dispatch(login({ username, password }));
  }
  // ...
}
```


## おわりに

ちょっと長くなってしまいましたが2つのmiddlewareを通じてどうやって肥大化、複雑化する非同期処理、副作用コードと戦っていくかを示しました。middlewareはStoreに直接アクセスできることから、やろうと思えば何でもできちゃうところが強力でもあり、混乱のもとにもなりえます。幸いなことにmiddlewareを細かく分割しても `applyMiddleware` で簡単に結合できますし、困ることはありません。またmiddlewareにすると他のプロジェクトでも同じパターンが使えることが多いです。

**結論: ComponentとかReducerで変なことするよりmiddlewareでやった方がマシ**

さて、素直にその場で書き下せないちょっとした処理をひたすらmiddlewareとして切り出していくとどうなるのか。それって Cycle.js みたいになるんですかね？ Cycle.js はまだチュートリアル読んだ程度なのよくわかっていませんが。