reduxの非同期アクションをジェネレーター関数で処理するredux-sagaを試す

redux-sagaが盛り上がってるらしいので触ってみました。

[yelouafi/redux-saga: An alternative side effect model for Redux apps](https://github.com/yelouafi/redux-saga)

redux-sagaはreduxにおける非同期アクションをジェネレーター関数を用いて、同期なスタイルで扱うことができるミドルウェアです。
公式に日本語のドキュメントもあります。
翻訳してくれた@kuyさんに感謝です。

[redux-saga/README_ja.md at master · yelouafi/redux-saga](https://github.com/yelouafi/redux-saga/blob/master/README_ja.md)

詳しい日本語の解説記事はこれから出てくることを期待して、とりあえず触ってみます。
reactと一緒に使うことがほとんどだと思いますが、最小構成で試したいのでreduxとredux-sagaのみでnode 6.1.0にて試してみました。


## インストール

```
$ npm init -y
$ npm i -S redux redux-saga
```

## reduxのみで雛形

カウンターのサンプルを書いてみます。

まずはreduxのみで基本形を書いてみます。

```js:index.js
const {createStore} = require('redux');

function counter(state = 0, action) {
	switch (action.type) {
		case 'INCREMENT':
			return state + 1;
		case 'DECREMENT':
			return state - 1;
		default:
			return state;
	}
}

const store = createStore(counter);

store.subscribe(() => {
	console.log(store.getState());
});

const action = type => store.dispatch({type});

action('INCREMENT');
action('INCREMENT');
action('INCREMENT');
action('DECREMENT');
```


```
$ node index.js
1
2
3
2
```

## redux-sagaによる非同期処理

ここに1秒後にstateを1つ増やす`INCREMENT_ASYNC`を追加してみます。

```index.js
'use strict';
const {createStore, applyMiddleware} = require('redux');
const reduxSaga = require('redux-saga');
const createSagaMiddleware = reduxSaga.default;
const {takeEvery, delay, effects} = reduxSaga;
const {put} = effects;

function counter(state = 0, action) {
	switch (action.type) {
		case 'INCREMENT':
			return state + 1;
		case 'DECREMENT':
			return state - 1;
		default:
			return state;
	}
}

function * incrementAsync() {
	yield delay(1000);
	yield put({type: 'INCREMENT'});
}

function * watchIncrementAsync() {
	yield * takeEvery('INCREMENT_ASYNC', incrementAsync);
}

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
	counter,
	applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(watchIncrementAsync);

store.subscribe(() => {
	console.log(store.getState());
});

const action = type => store.dispatch({type});

action('INCREMENT_ASYNC');
action('INCREMENT');
action('INCREMENT');
action('INCREMENT');
action('DECREMENT');
action('INCREMENT_ASYNC');
```

```
$ node index.js
0
1
2
3
2
2
3
4
```

少しわかりづらいですが、INCREMENT_ASYNCアクションが２回発行されたので最終的にstateが4になっています。
正しく動作しているようです。

簡単な解説をします。
redux-sagaがジェネレーター関数で非同期処理を同期処理に処理できます。
delayで1秒待ち、putでアクションを発行できます。
takeEveryは`INCREMENT_ASYNC`アクションがきたら、並列でincrementAsyncを処理します。

他のreduxのミドルウェアと同様にapplyMiddlewareでsagaミドルウェアを登録し、`run`でsagaを走らせます。

## 複数のsagaの追加

今のままだとロガーが心もとないのでredux-logger風のロガーを作ってみます。
アクションとアクション発行前と発行後のstoreの状態を表示してみます。

```index.js
'use strict';
const {createStore, applyMiddleware} = require('redux');
const reduxSaga = require('redux-saga');
const createSagaMiddleware = reduxSaga.default;
const {takeEvery, delay, effects} = reduxSaga;
const {put, take, select, fork} = effects;

function counter(state = 0, action) {
	switch (action.type) {
		case 'INCREMENT':
			return state + 1;
		case 'DECREMENT':
			return state - 1;
		default:
			return state;
	}
}

function * incrementAsync() {
	yield delay(1000);
	yield put({type: 'INCREMENT'});
}

function * watchIncrementAsync() {
	yield * takeEvery('INCREMENT_ASYNC', incrementAsync);
}

function * watchAndLog() {
	while (true) {
		console.log('before', yield select());
		const action = yield take('*');
		console.log('action', action);
		console.log('after ', yield select());
	}
}

function * rootSaga() {
	yield fork(watchAndLog);
	yield fork(watchIncrementAsync);
}

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
	counter,
	applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);

const action = type => store.dispatch({type});

action('INCREMENT_ASYNC');
action('INCREMENT');
action('INCREMENT');
action('INCREMENT');
action('DECREMENT');
action('INCREMENT_ASYNC');
```

```
$ node index.js
before 0
action { type: 'INCREMENT_ASYNC' }
after  0
before 0
action { type: 'INCREMENT' }
after  1
before 1
action { type: 'INCREMENT' }
after  2
before 2
action { type: 'INCREMENT' }
after  3
before 3
action { type: 'DECREMENT' }
after  2
before 2
action { type: 'INCREMENT_ASYNC' }
after  2
before 2
action { type: 'INCREMENT' }
after  3
before 3
action { type: 'INCREMENT' }
after  4
before 4
```

INCREMENT_ASYNCアクションが２回発行されているので、最後にINCREMENTアクションが２回発行され、stateが増えるのがわかりますね。

さて、`take`, `select`, `fork`といった関数が出てきました。
takeはアクションを待ち受けることができます。
今回はすべてのアクションを待っていますが、take('アクション')で特定のアクションがきたときに作用する関数を簡潔に書くことができます。
forkはsagaを並列に実行し、selectはstateを取得できます。

## まとめ
redux-sagaはジェネレーター関数によって非常にシンプルに非同期処理を書くことができます。
redux-thunkと使う状況であれば、これからは間違いなくredux-sagaを使うと思います。

今回のコードはgithubに公開しています。
間違った使い方をしてる等あれば、アドバイスをくださると嬉しいです。

[akameco/redux-saga-example](https://github.com/akameco/redux-saga-example)

## 参考
[yelouafi/redux-saga: An alternative side effect model for Redux apps](https://github.com/yelouafi/redux-saga)
[redux-saga/README_ja.md at master · yelouafi/redux-saga](https://github.com/yelouafi/redux-saga/blob/master/README_ja.md)
[akameco/redux-saga-example](https://github.com/akameco/redux-saga-example)