Redux: redux-sagaで非同期処理を試す ー 公式のカウンター作例を使って

[redux-saga](https://github.com/redux-saga/redux-saga/blob/master/README_ja.md)は、非同期処理が同期的に書け、複数の処理を同時並行で実行できるライブラリです。GitHubの[`redux-saga/examples/`](https://github.com/redux-saga/redux-saga/)には、簡単なカウンターの作例が3つ収められています。その中でもっともシンプルな[`redux-saga/examples/counter-vanilla`](https://github.com/redux-saga/redux-saga/tree/master/examples/counter-vanilla)は、`index.html`ひとつにまとめられていて、ビルドが要りません。この作例(サンプル001)のコードを段階に分けてご説明しましょう。さらにReactも加えて、コンポーネントで組み立てた作例は「[React + Redux: redux-sagaで非同期処理を試す](http://fumiononaka.com/Business/html5/FN1912001.html)」で解説しています。ご興味のある方はご覧ください。

#### サンプル001■Redux: Counter example with redux-saga
<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="FumioNonaka" data-slug-hash="jOEbWmW" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Redux: Counter example with redux-saga">
  <span>See the Pen <a href="https://codepen.io/FumioNonaka/pen/jOEbWmW">
  Redux: Counter example with redux-saga</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# Reduxでカウンターをつくる
JavaScriptコードを書きはじめる前に、CDNからのReduxとredux-sagaのライブラリの読み込みです。公式作例は[unpkg](https://unpkg.com)を用いています。けれど、redux-sagaのバージョンが1.0.0-beta.1と少し古いようです。そこで、新しいライブラリ(1.0.5)が手に入る[cdnjs](https://cdnjs.com)に差し替えましょう。

```html
<!-- <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
<script src="https://unpkg.com/redux-saga@1.0.0-beta.1/dist/redux-saga.min.js"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.4/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-saga/1.0.5/redux-saga.umd.min.js"></script>
```

まず、redux-sagaは使わずに、Reduxのアプリケーションをつくります。数値を含むカウンターのテキストと、ボタンは[+][-][Increment if odd]の4つです。

```html
<div>
	<p>
	Clicked: <span id="value">0</span> times
	<button id="increment">+</button>
	<button id="decrement">-</button>
	<button id="incrementIfOdd">Increment if odd</button>
	</p>
</div>
```

Reduxアプリケーションでは、処理の開始をActionで知らせます。Actionを受け取って、どのActionか`type`で判別して結果を返すのが、つぎのReducerです。今回`state`は数値で、その加算あるいは減算が行われます。

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

Reduxはアプリケーションにひとつだけ備わるStoreが状態を一手に管理します。そのStoreをつくるのが[`createStore()`](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer)です(「React + Redux入門 02: フィールドに入力したテキストを項目リストに加える」02「[Storeをつくる](http://www.fumiononaka.com/Business/html5/FN1908002.html#sec02)」参照)。引数にはReducerを渡します。

ボタンクリック(`click`イベント)のリスナー関数から呼び出しているのが、Actionを送り出す[`dispatch()`](https://redux.js.org/api/store#dispatchaction)です。引数のActionオブジェクトは、少なくとも識別のためのプロパティ`type`をもたなければなりません。これが、前掲Reducerによる判別に使われるのです。

[`subscribe()`](https://redux.js.org/api/store#subscribelistener)は、Actionが送られるたびに、引数のリスナー関数を呼び出します。Storeがもつ`state`を参照するのが[`getState()`](https://redux.js.org/api/store#getstate)です。

```js
var store = Redux.createStore(
	counter
)
var valueEl = document.getElementById('value')
function render() {
	valueEl.innerHTML = store.getState().toString()
}
render()
store.subscribe(render)
document.getElementById('increment')
.addEventListener('click', function () {
	store.dispatch({ type: 'INCREMENT' })
})
document.getElementById('decrement')
.addEventListener('click', function () {
	store.dispatch({ type: 'DECREMENT' })
})
document.getElementById('incrementIfOdd')
.addEventListener('click', function () {
	if (store.getState() % 2 !== 0) {
	store.dispatch({ type: 'INCREMENT' })
	}
})
```

このコードにより、ボタンごとに決められたActionが送り出され、カウンタの数値となる`state`の値が変わります。すると、ページのカウンタの数がその値で書き替えるという仕組みです。なお、[Increment if odd]のボタンは、カウンタが奇数のときのみカウントアップします。


# redux-sagaで非同期処理を行う
つぎに、redux-sagaで非同期の処理を行いましょう。SagaはReducerに替わって、Storeに送られるあらかじめ決められたActionを監視します。コードは以下のとおりてす。Sagaのアプリケーションへの組み込みと起動は、このあとご説明します。

非同期の処理を扱うため、ジェネレーター関数の[function*](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function*)宣言を用いることにご注目ください(「[ジェネレーター関数](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Iterators_and_Generators#Generators)」参照)。`rootSaga()`が、あとでミドルウェアから実行される関数です。[`yield`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/yield)キーワードに定めた[`takeEvery()`](https://redux-saga.js.org/docs/api/#takeeverypattern-saga-args)は、第1引数(`INCREMENT_ASYNC`)のActionを監視し、送り出されるたびに第2引数(`incrementAsync()`)の関数を呼び出します。

関数`incrementAsync()`は、[`delay()`](https://redux-saga.js.org/docs/api/#delayms-val)で引数のミリ秒数実行を止めます。実行再開後、Storeに引数オブジェクトのActionを送り出すのが[`put()`](https://redux-saga.js.org/docs/api/#putaction)です。つまり、1秒待ってカウントアップされることになります。

```js
const effects = ReduxSaga.effects
function* incrementAsync() {
	yield effects.delay(1000)
	yield effects.put({type: 'INCREMENT'})
}
function* counterSaga() {
	yield effects.takeEvery('INCREMENT_ASYNC', incrementAsync)
}
```

それでは、SagaをStoreにミドルウェアとして組み込みます。Sagaミドルウェアをつくるのが、[`createSagaMiddleware()`](https://redux-saga.js.org/docs/api/#createsagamiddlewareoptions)です。ミドルウェアは、[`applyMiddleware()`](https://redux.js.org/api/applymiddleware#applymiddlewaremiddleware)でStoreに適用します。そのうえで、[`run()`](https://redux-saga.js.org/docs/api/#middlewarerunsaga-args)によりSagaを実行してください。前掲関数`render()`を定義するコードの前に加えるのが、つぎのステートメントです。

```js
const createSagaMiddleware = ReduxSaga.default
const sagaMiddleware = createSagaMiddleware()
var store = Redux.createStore(
	counter,
	Redux.applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(counterSaga)
```

こうして、前掲サンプル001の作例が書き上がりました。


# コードの構文を改める
サンプル001にはECMAScript 2015の`const`と古い`var`宣言が混じっています。コード全体をECMAScript 2015の構文に改めましょう[^1]。まず、宣言`var`は定数`const`に書き替えます(値を上書きしませんので、`let`は使いません)。

```js
// var store = Redux.createStore(
const store = Redux.createStore(
	counter,
	Redux.applyMiddleware(sagaMiddleware)
)

// var valueEl = document.getElementById('value')
const valueEl = document.getElementById('value')
```

つぎに、無名関数にはアロー関数式`=>`を用いましょう。

```js
document.getElementById('increment')
// .addEventListener('click', function () {
.addEventListener('click', () =>
	store.dispatch({ type: 'INCREMENT' })
)
document.getElementById('decrement')
// .addEventListener('click', function () {
.addEventListener('click', () =>
	store.dispatch({ type: 'DECREMENT' })
)
document.getElementById('incrementIfOdd')
// .addEventListener('click', function () {
.addEventListener('click', () => {
	if (store.getState() % 2 !== 0) {
		store.dispatch({ type: 'INCREMENT' })
	}
})
document.getElementById('incrementAsync')
// .addEventListener('click', function () {
.addEventListener('click', () =>
	store.dispatch({ type: 'INCREMENT_ASYNC' })
)
```

これでECMAScript 2015の構文にできました。あとひとつ、プロパティの`innerHTML`は`textContent`に差し替えます。セキュリティやパフォーマンスの点から、後者で足りる場合にはこちらを使った方がよいからです。MDNのリファレンスから、引用しておきましょう。

「element.innerHTML」「[セキュリティの考慮事項](https://developer.mozilla.org/docs/Web/API/Element/innerHTML#Security_considerations)」
> プレーンテキストを挿入するときには`innerHTML`を使用せず、代わりに`Node.textContent`を使用することをお勧めします。これは渡されたコンテンツを HTML として解釈するのではなく、生テキストとして挿入します。

「Node.textContent」「[`innerHTML`との違い](https://developer.mozilla.org/ja/docs/Web/API/Node/textContent#Differences_from_innerHTML)」
> `textContent`はパフォーマンスを向上させる場合があります。テキストが HTML として解析されないためです。さらに、`textContent`を使用することで XSS 攻撃を防ぐことができます。

```js
function render() {
	// valueEl.innerHTML = store.getState().toString()
	valueEl.textContent = store.getState().toString()
}
```

構文を改めたサンプル002もCodePenに公開しました。

#### サンプル002■ Redux + ES6: Counter example with redux-saga
<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="FumioNonaka" data-slug-hash="qBEZJgL" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Redux + ES6: Counter example with redux-saga">
  <span>See the Pen <a href="https://codepen.io/FumioNonaka/pen/qBEZJgL">
  Redux + ES6: Counter example with redux-saga</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


[^1]: GitHubのREADME_ja.mdで「[counter-vanilla](https://github.com/redux-saga/redux-saga/blob/master/README_ja.md#counter-vanilla)」の説明を見ると、「ES2015を使っていない素のJavaScriptとUMDビルドを使用したデモ」と書かれています。けれど、英語の「[counter-vanilla](https://github.com/redux-saga/redux-saga/blob/master/README.md#counter-vanilla)」はECMAScript 2015には触れていません。おそらく、日本語版の説明が古いのでしょう。