初心者向けチュートリアル
このチュートリアルの目的
このチュートリアルでは、(うまくいけば)わかりやすい方法でredux-sagaを紹介します。
入門チュートリアルでは、Reduxリポジトリの簡単なカウンターデモを使用します。このアプリケーションは非常に基本的なものですが、詳細に埋もれることなくredux-sagaの基本概念を示すのに適しています。
初期設定
始める前に、チュートリアルリポジトリをクローンしてください。
このチュートリアルの最終コードは、
sagas
ブランチにあります。
次に、コマンドラインで次を実行します
$ cd redux-saga-beginner-tutorial
$ npm install
アプリケーションを起動するには、次を実行します
$ npm start
コンパイルが完了したら、ブラウザでhttp://localhost:9966を開きます。
最も基本的なユースケースから始めます。カウンターをインクリメント
およびデクリメント
するための2つのボタンです。後で、非同期呼び出しを紹介します。
うまくいけば、インクリメント
とデクリメント
の2つのボタンと、下にカウンター: 0
と表示されるはずです。
アプリケーションの実行で問題が発生した場合は、チュートリアルリポジトリで遠慮なくissueを作成してください。
Hello Sagas!
最初のSagaを作成します。伝統に従い、Sagaの「Hello, world」バージョンを作成します。
ファイルsagas.js
を作成し、次のスニペットを追加します
export function* helloSaga() {
console.log('Hello Sagas!')
}
それほど怖いことはありません。通常の関数(*
を除く)です。コンソールに挨拶メッセージを表示するだけです。
Sagaを実行するには、以下を行う必要があります。
- 実行するSagaのリストを含むSagaミドルウェアを作成します(今のところ、
helloSaga
が1つだけです)。 - SagaミドルウェアをReduxストアに接続します。
main.js
を変更します。
// ...
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
// ...
import { helloSaga } from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(helloSaga)
const action = type => store.dispatch({type})
// rest unchanged
まず、./sagas
モジュールからSagaをインポートします。次に、redux-saga
ライブラリによってエクスポートされたファクトリ関数createSagaMiddleware
を使用してミドルウェアを作成します。
helloSaga
を実行する前に、applyMiddleware
を使用してミドルウェアをストアに接続する必要があります。次に、sagaMiddleware.run(helloSaga)
を使用してSagaを開始できます。
今のところ、Sagaは特別なことは何もしていません。メッセージをログに記録して終了するだけです。
非同期呼び出しを行う
次に、元のカウンターデモに近いものを追加しましょう。非同期呼び出しを説明するために、クリックから1秒後にカウンターをインクリメントする別のボタンを追加します。
まず、UIコンポーネントに追加のボタンとコールバックonIncrementAsync
を提供します。
Counter.js
を変更します。
const Counter = ({ value, onIncrement, onDecrement, onIncrementAsync }) =>
<div>
<button onClick={onIncrementAsync}>
Increment after 1 second
</button>
{' '}
<button onClick={onIncrement}>
Increment
</button>
{' '}
<button onClick={onDecrement}>
Decrement
</button>
<hr />
<div>
Clicked: {value} times
</div>
</div>
次に、コンポーネントのonIncrementAsync
をストアアクションに接続する必要があります。
main.js
モジュールを次のように変更します
function render() {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}
redux-thunkとは異なり、コンポーネントはプレーンなオブジェクトアクションをディスパッチすることに注意してください。
次に、非同期呼び出しを実行する別のSagaを紹介します。ユースケースは次のとおりです。
INCREMENT_ASYNC
アクションごとに、次のタスクを開始します。
- 1秒待ってからカウンターをインクリメントします
次のコードをsagas.js
モジュールに追加します
import { put, takeEvery } from 'redux-saga/effects'
const delay = (ms) => new Promise(res => setTimeout(res, ms))
// ...
// Our worker Saga: will perform the async increment task
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
説明の時間です。
指定されたミリ秒後に解決されるPromiseを返すdelay
関数を作成します。この関数を使用して、ジェネレーターを*ブロック*します。
Sagaは、redux-sagaミドルウェアにオブジェクトを*yield*するジェネレーター関数として実装されます。yieldされたオブジェクトは、ミドルウェアによって解釈される一種の命令です。Promiseがミドルウェアにyieldされると、ミドルウェアはPromiseが完了するまでSagaを中断します。上記の例では、incrementAsync
Sagaは、delay
によって返されたPromiseが解決されるまで中断されます。これは1秒後に発生します。
Promiseが解決されると、ミドルウェアはSagaを再開し、次のyieldまでコードを実行します。この例では、次のステートメントは、put({type: 'INCREMENT'})
を呼び出した結果である別のyieldオブジェクトです。これは、ミドルウェアにINCREMENT
アクションをディスパッチするように指示します。
put
は、*エフェクト*と呼ばれるものの1つの例です。エフェクトは、ミドルウェアによって満たされる命令を含むプレーンなJavaScriptオブジェクトです。ミドルウェアがSagaによってyieldされたエフェクトを取得すると、エフェクトが満たされるまでSagaは一時停止されます。
つまり、incrementAsync
Sagaは、delay(1000)
の呼び出しによって1秒間スリープし、次にINCREMENT
アクションをディスパッチします。
次に、別のSaga watchIncrementAsync
を作成しました。redux-saga
によって提供されるヘルパー関数であるtakeEvery
を使用して、ディスパッチされたINCREMENT_ASYNC
アクションをリッスンし、毎回incrementAsync
を実行します。
これで2つのSagaができたので、両方を一度に開始する必要があります。そのためには、他のSagaを開始する役割を果たすrootSaga
を追加します。同じファイルsagas.js
で、次のようにファイルをリファクタリングします
import { put, takeEvery, all } from 'redux-saga/effects'
export const delay = (ms) => new Promise(res => setTimeout(res, ms))
export function* helloSaga() {
console.log('Hello Sagas!')
}
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
// notice how we now only export the rootSaga
// single entry point to start all Sagas at once
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
このSagaは、2つのSagaであるhelloSaga
とwatchIncrementAsync
を呼び出した結果の配列をyieldします。これは、結果の2つのジェネレーターが並行して開始されることを意味します。これで、main.js
のルートSagaでsagaMiddleware.run
を呼び出すだけです。
// ...
import rootSaga from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = ...
sagaMiddleware.run(rootSaga)
// ...
コードをテスト可能にする
incrementAsync
Sagaをテストして、目的のタスクを実行していることを確認します。
別のファイルsagas.spec.js
を作成します
import test from 'tape'
import { incrementAsync } from './sagas'
test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()
// now what ?
})
incrementAsync
はジェネレーター関数です。実行すると、イテレーターオブジェクトが返されます。イテレーターのnext
メソッドは、次の形状のオブジェクトを返します。
gen.next() // => { done: boolean, value: any }
value
フィールドには、yieldされた式、つまりyield
後の式の結果が含まれています。done
フィールドは、ジェネレーターが終了したか、まだ「yield」式があるかを示します。
incrementAsync
の場合、ジェネレーターは2つの値を連続してyieldします。
yield delay(1000)
yield put({type: 'INCREMENT'})
したがって、ジェネレーターのnextメソッドを連続して3回呼び出すと、次の結果が得られます。
gen.next() // => { done: false, value: <result of calling delay(1000)> }
gen.next() // => { done: false, value: <result of calling put({type: 'INCREMENT'})> }
gen.next() // => { done: true, value: undefined }
最初の2回の呼び出しは、yield式の結果を返します。3回目の呼び出しでは、それ以上のyieldがないため、done
フィールドがtrueに設定されます。また、incrementAsync
ジェネレーターは何も返さないため(return
ステートメントがない)、value
フィールドはundefined
に設定されます。
したがって、incrementAsync
内のロジックをテストするには、返されたジェネレーターを反復処理し、ジェネレーターによってyieldされた値をチェックする必要があります。
import test from 'tape'
import { incrementAsync } from './sagas'
test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()
assert.deepEqual(
gen.next(),
{ done: false, value: ??? },
'incrementAsync should return a Promise that will resolve after 1 second'
)
})
問題は、delay
の戻り値をどのようにテストするかです。Promiseで単純な等価性テストを行うことはできません。delay
が*通常の*値を返した場合、テストは簡単になったでしょう。
さて、redux-saga
は、上記のステートメントを可能にする方法を提供します。incrementAsync
内でdelay(1000)
を直接呼び出す代わりに、*間接的に*呼び出し、後続の深い比較を可能にするためにエクスポートします。
import { put, takeEvery, all, call } from 'redux-saga/effects'
export const delay = (ms) => new Promise(res => setTimeout(res, ms))
// ...
export function* incrementAsync() {
// use the call Effect
yield call(delay, 1000)
yield put({ type: 'INCREMENT' })
}
yield delay(1000)
を実行する代わりに、yield call(delay, 1000)
を実行しています。違いは何ですか?
最初の場合、yield式delay(1000)
は、next
の呼び出し元に渡される前に評価されます(呼び出し元は、コードを実行するときはミドルウェアになる可能性があります。ジェネレーター関数を実行し、返されたジェネレーターを反復処理するテストコードになる可能性もあります)。したがって、呼び出し元が取得するのは、上記のテストコードのようにPromiseです。
2番目の場合、yield式call(delay, 1000)
は、next
の呼び出し元に渡されるものです。call
は、put
と同様に、指定された引数を使用して指定された関数を呼び出すようにミドルウェアに指示するエフェクトを返します。実際、put
もcall
も、それ自体でディスパッチまたは非同期呼び出しを実行しません。それらはプレーンなJavaScriptオブジェクトを返します。
put({type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} }
call(delay, 1000) // => { CALL: {fn: delay, args: [1000]}}
何が起こるかというと、ミドルウェアはyieldされた各エフェクトのタイプを調べ、そのエフェクトを満たす方法を決定します。エフェクトタイプがPUT
の場合、ストアにアクションをディスパッチします。エフェクトがCALL
の場合、指定された関数を呼び出します。
エフェクトの作成とエフェクトの実行のこの分離により、ジェネレーターを驚くほど簡単な方法でテストすることが可能になります。
import test from 'tape'
import { put, call } from 'redux-saga/effects'
import { incrementAsync, delay } from './sagas'
test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()
assert.deepEqual(
gen.next().value,
call(delay, 1000),
'incrementAsync Saga must call delay(1000)'
)
assert.deepEqual(
gen.next().value,
put({type: 'INCREMENT'}),
'incrementAsync Saga must dispatch an INCREMENT action'
)
assert.deepEqual(
gen.next(),
{ done: true, value: undefined },
'incrementAsync Saga must be done'
)
assert.end()
})
put
とcall
はプレーンなオブジェクトを返すため、テストコードで同じ関数を再利用できます。また、incrementAsync
のロジックをテストするには、ジェネレーターを反復処理し、その値でdeepEqual
テストを行います。
上記のテストを実行するには、次を実行します
$ npm test
これにより、コンソールに結果が報告されるはずです。