redux-sagaは、非同期処理が同期的に書け、複数の処理を同時並行で実行できるライブラリです。GitHubのredux-saga/examples/
には、簡単なカウンターの作例が3つ収められています。その中でもっともシンプルなredux-saga/examples/counter-vanilla
は、index.html
ひとつにまとめられていて、ビルドが要りません。この作例(サンプル001)のコードを段階に分けてご説明しましょう。さらにReactも加えて、コンポーネントで組み立てた作例は「React + Redux: redux-sagaで非同期処理を試す」で解説しています。ご興味のある方はご覧ください。
サンプル001■Redux: Counter example with redux-saga
See the Pen Redux: Counter example with redux-saga by Fumio Nonaka (@FumioNonaka) on CodePen.
Reduxでカウンターをつくる
JavaScriptコードを書きはじめる前に、CDNからのReduxとredux-sagaのライブラリの読み込みです。公式作例はunpkgを用いています。けれど、redux-sagaのバージョンが1.0.0-beta.1と少し古いようです。そこで、新しいライブラリ(1.0.5)が手に入るcdnjsに差し替えましょう。
<!-- <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つです。
<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
は数値で、その加算あるいは減算が行われます。
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()
です(「React + Redux入門 02: フィールドに入力したテキストを項目リストに加える」02「Storeをつくる」参照)。引数にはReducerを渡します。
ボタンクリック(click
イベント)のリスナー関数から呼び出しているのが、Actionを送り出すdispatch()
です。引数のActionオブジェクトは、少なくとも識別のためのプロパティtype
をもたなければなりません。これが、前掲Reducerによる判別に使われるのです。
subscribe()
は、Actionが送られるたびに、引数のリスナー関数を呼び出します。Storeがもつstate
を参照するのがgetState()
です。
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*宣言を用いることにご注目ください(「ジェネレーター関数」参照)。rootSaga()
が、あとでミドルウェアから実行される関数です。yield
キーワードに定めたtakeEvery()
は、第1引数(INCREMENT_ASYNC
)のActionを監視し、送り出されるたびに第2引数(incrementAsync()
)の関数を呼び出します。
関数incrementAsync()
は、delay()
で引数のミリ秒数実行を止めます。実行再開後、Storeに引数オブジェクトのActionを送り出すのがput()
です。つまり、1秒待ってカウントアップされることになります。
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()
です。ミドルウェアは、applyMiddleware()
でStoreに適用します。そのうえで、run()
によりSagaを実行してください。前掲関数render()
を定義するコードの前に加えるのが、つぎのステートメントです。
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
は使いません)。
// var store = Redux.createStore(
const store = Redux.createStore(
counter,
Redux.applyMiddleware(sagaMiddleware)
)
// var valueEl = document.getElementById('value')
const valueEl = document.getElementById('value')
つぎに、無名関数にはアロー関数式=>
を用いましょう。
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」「セキュリティの考慮事項」
プレーンテキストを挿入するときには
innerHTML
を使用せず、代わりにNode.textContent
を使用することをお勧めします。これは渡されたコンテンツを HTML として解釈するのではなく、生テキストとして挿入します。
「Node.textContent」「innerHTML
との違い」
textContent
はパフォーマンスを向上させる場合があります。テキストが HTML として解析されないためです。さらに、textContent
を使用することで XSS 攻撃を防ぐことができます。
function render() {
// valueEl.innerHTML = store.getState().toString()
valueEl.textContent = store.getState().toString()
}
構文を改めたサンプル002もCodePenに公開しました。
サンプル002■ Redux + ES6: Counter example with redux-saga
See the Pen Redux + ES6: Counter example with redux-saga by Fumio Nonaka (@FumioNonaka) on CodePen.
-
GitHubのREADME_ja.mdで「counter-vanilla」の説明を見ると、「ES2015を使っていない素のJavaScriptとUMDビルドを使用したデモ」と書かれています。けれど、英語の「counter-vanilla」はECMAScript 2015には触れていません。おそらく、日本語版の説明が古いのでしょう。 ↩