redux-saga
Advanced tools
Comparing version 0.2.2 to 0.3.0
@@ -6,10 +6,51 @@ 'use strict'; | ||
}); | ||
exports.SAGA_NOT_A_GENERATOR_ERROR = undefined; | ||
exports.SAGA_NOT_A_GENERATOR_ERROR = exports.join = exports.fork = exports.cps = exports.call = exports.race = exports.put = exports.take = undefined; | ||
var _utils = require('./utils'); | ||
var _io = require('./io'); | ||
var _io2 = _interopRequireDefault(_io); | ||
Object.defineProperty(exports, 'take', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.take; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'put', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.put; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'race', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.race; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'call', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.call; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'cps', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.cps; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'fork', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.fork; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'join', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.join; | ||
} | ||
}); | ||
var _utils = require('./utils'); | ||
var _proc = require('./proc'); | ||
@@ -37,6 +78,3 @@ | ||
// wait for the current tick, to let other middlewares (e.g. logger) run | ||
Promise.resolve(1).then(function () { | ||
(0, _proc2.default)(saga(_io2.default, getState), subscribe, dispatch, saga.name); | ||
}); | ||
(0, _proc2.default)(saga(getState), subscribe, dispatch, saga.name); | ||
}); | ||
@@ -43,0 +81,0 @@ |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,10 +6,23 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.as = undefined; | ||
exports.as = exports.JOIN_ARG_ERROR = exports.FORK_ARG_ERROR = exports.CPS_FUNCTION_ARG_ERROR = exports.CALL_FUNCTION_ARG_ERROR = undefined; | ||
exports.matcher = matcher; | ||
exports.take = take; | ||
exports.put = put; | ||
exports.race = race; | ||
exports.call = call; | ||
exports.cps = cps; | ||
exports.fork = fork; | ||
exports.join = join; | ||
var _utils = require('./utils'); | ||
var _utils = require("./utils"); | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
var CALL_FUNCTION_ARG_ERROR = exports.CALL_FUNCTION_ARG_ERROR = "io.call first argument must be a function"; | ||
var CPS_FUNCTION_ARG_ERROR = exports.CPS_FUNCTION_ARG_ERROR = "io.cps first argument must be a function"; | ||
var FORK_ARG_ERROR = exports.FORK_ARG_ERROR = "io.fork first argument must be a generator function or an iterator"; | ||
var JOIN_ARG_ERROR = exports.JOIN_ARG_ERROR = "io.join argument must be a valid task (a result of io.fork)"; | ||
var IO = Symbol('IO'); | ||
var TAKE = 'TAKE'; | ||
@@ -20,2 +33,4 @@ var PUT = 'PUT'; | ||
var CPS = 'CPS'; | ||
var FORK = 'FORK'; | ||
var JOIN = 'JOIN'; | ||
@@ -55,27 +70,46 @@ var effect = function effect(type, payload) { | ||
exports.default = { | ||
take: function take(pattern) { | ||
return effect(TAKE, _utils.is.undef(pattern) ? '*' : pattern); | ||
}, | ||
put: function put(ac) { | ||
return effect(PUT, ac); | ||
}, | ||
race: function race(effects) { | ||
return effect(RACE, effects); | ||
}, | ||
call: function call(fn) { | ||
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
args[_key - 1] = arguments[_key]; | ||
} | ||
function take(pattern) { | ||
return effect(TAKE, _utils.is.undef(pattern) ? '*' : pattern); | ||
} | ||
return effect(CALL, { fn: fn, args: args }); | ||
}, | ||
cps: function cps(fn) { | ||
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { | ||
args[_key2 - 1] = arguments[_key2]; | ||
} | ||
function put(action) { | ||
return effect(PUT, action); | ||
} | ||
return effect(CPS, { fn: fn, args: args }); | ||
function race(effects) { | ||
return effect(RACE, effects); | ||
} | ||
function call(fn) { | ||
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
args[_key - 1] = arguments[_key]; | ||
} | ||
}; | ||
(0, _utils.check)(fn, _utils.is.func, CALL_FUNCTION_ARG_ERROR); | ||
return effect(CALL, { fn: fn, args: args }); | ||
} | ||
function cps(fn) { | ||
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { | ||
args[_key2 - 1] = arguments[_key2]; | ||
} | ||
(0, _utils.check)(fn, _utils.is.func, CPS_FUNCTION_ARG_ERROR); | ||
return effect(CPS, { fn: fn, args: args }); | ||
} | ||
function fork(task) { | ||
for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { | ||
args[_key3 - 1] = arguments[_key3]; | ||
} | ||
return effect(FORK, { task: task, args: args }); | ||
} | ||
function join(taskDesc) { | ||
if (!taskDesc[_utils.TASK]) throw new Error(JOIN_ARG_ERROR); | ||
return effect(JOIN, taskDesc); | ||
} | ||
var as = exports.as = { | ||
@@ -96,3 +130,9 @@ take: function take(effect) { | ||
return effect && effect[IO] && effect[CPS]; | ||
}, | ||
fork: function fork(effect) { | ||
return effect && effect[IO] && effect[FORK]; | ||
}, | ||
join: function join(effect) { | ||
return effect && effect[IO] && effect[JOIN]; | ||
} | ||
}; |
@@ -17,3 +17,3 @@ 'use strict'; | ||
var NOT_ITERATOR_ERROR = exports.NOT_ITERATOR_ERROR = "proc argument must be an iterator"; | ||
var NOT_ITERATOR_ERROR = exports.NOT_ITERATOR_ERROR = "proc first argument must be an iterator"; | ||
@@ -26,3 +26,3 @@ function proc(iterator) { | ||
if (!_utils.is.iterator(iterator)) throw new Error(NOT_ITERATOR_ERROR); | ||
(0, _utils.check)(iterator, _utils.is.iterator, NOT_ITERATOR_ERROR); | ||
@@ -42,2 +42,3 @@ var deferredInput = undefined, | ||
next(); | ||
iterator._isRunning = true; | ||
return endP; | ||
@@ -59,2 +60,4 @@ | ||
//console.log('return', name, result.value) | ||
iterator._isRunning = false; | ||
iterator._result = result.value; | ||
unsubscribe(); | ||
@@ -65,2 +68,4 @@ deferredEnd.resolve(result.value); | ||
//console.log('catch', name, err) | ||
iterator._isRunning = false; | ||
iterator._error = err; | ||
unsubscribe(); | ||
@@ -72,6 +77,4 @@ deferredEnd.reject(err); | ||
function runEffect(effect) { | ||
var _data; | ||
var data = undefined; | ||
return _utils.is.array(effect) ? Promise.all(effect.map(runEffect)) : _utils.is.iterator(effect) ? proc(effect, subscribe, dispatch) : (data = _io.as.take(effect)) ? runTakeEffect(data) : (data = _io.as.put(effect)) ? Promise.resolve(dispatch(data)) : (data = _io.as.race(effect)) ? runRaceEffect(data) : (data = _io.as.call(effect)) ? (_data = data).fn.apply(_data, _toConsumableArray(data.args)) : (data = _io.as.cps(effect)) ? runCPSEffect(data) : /* resolve anything else */Promise.resolve(effect); | ||
return _utils.is.array(effect) ? Promise.all(effect.map(runEffect)) : _utils.is.iterator(effect) ? proc(effect, subscribe, dispatch) : (data = _io.as.take(effect)) ? runTakeEffect(data) : (data = _io.as.put(effect)) ? Promise.resolve(dispatch(data)) : (data = _io.as.race(effect)) ? runRaceEffect(data) : (data = _io.as.call(effect)) ? runCallEffect(data.fn, data.args) : (data = _io.as.cps(effect)) ? runCPSEffect(data.fn, data.args) : (data = _io.as.fork(effect)) ? runForkEffect(data.task, data.args) : (data = _io.as.join(effect)) ? runJoinEffect(data) : /* resolve anything else */Promise.resolve(effect); | ||
} | ||
@@ -85,10 +88,63 @@ | ||
function runCPSEffect(cps) { | ||
function runCallEffect(fn, args) { | ||
return !_utils.is.generator(fn) ? Promise.resolve(fn.apply(undefined, _toConsumableArray(args))) : proc(fn.apply(undefined, _toConsumableArray(args)), subscribe, dispatch); | ||
} | ||
function runCPSEffect(fn, args) { | ||
return new Promise(function (resolve, reject) { | ||
cps.fn.apply(cps, [].concat(_toConsumableArray(cps.args), [function (err, res) { | ||
fn.apply(undefined, _toConsumableArray(args.concat(function (err, res) { | ||
return _utils.is.undef(err) ? resolve(res) : reject(err); | ||
}])); | ||
}))); | ||
}); | ||
} | ||
function runForkEffect(task, args) { | ||
var _taskDesc; | ||
var _generator = undefined, | ||
_iterator = undefined; | ||
if (_utils.is.generator(task)) { | ||
_generator = task; | ||
_iterator = _generator.apply(undefined, _toConsumableArray(args)); | ||
} else if (_utils.is.iterator(task)) { | ||
// directly forking an iterator | ||
_iterator = task; | ||
} else { | ||
//simple effect: wrap in a generator | ||
_iterator = regeneratorRuntime.mark(function _callee() { | ||
return regeneratorRuntime.wrap(function _callee$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
_context.next = 2; | ||
return _utils.is.func(task) ? task.apply(undefined, _toConsumableArray(args)) : task; | ||
case 2: | ||
return _context.abrupt('return', _context.sent); | ||
case 3: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
} | ||
}, _callee, this); | ||
})(); | ||
} | ||
var _done = proc(_iterator, subscribe, dispatch); | ||
var taskDesc = (_taskDesc = {}, _defineProperty(_taskDesc, _utils.TASK, true), _defineProperty(_taskDesc, '_generator', _generator), _defineProperty(_taskDesc, '_iterator', _iterator), _defineProperty(_taskDesc, '_done', _done), _defineProperty(_taskDesc, 'name', _generator && _generator.name), _defineProperty(_taskDesc, 'isRunning', function isRunning() { | ||
return _iterator._isRunning; | ||
}), _defineProperty(_taskDesc, 'result', function result() { | ||
return _iterator._result; | ||
}), _defineProperty(_taskDesc, 'error', function error() { | ||
return _iterator._error; | ||
}), _taskDesc); | ||
return Promise.resolve(taskDesc); | ||
} | ||
function runJoinEffect(task) { | ||
return task._done; | ||
} | ||
function runRaceEffect(effects) { | ||
@@ -95,0 +151,0 @@ return Promise.race(Object.keys(effects).map(function (key) { |
@@ -6,3 +6,5 @@ 'use strict'; | ||
}); | ||
exports.check = check; | ||
exports.remove = remove; | ||
var TASK = exports.TASK = Symbol('TASK'); | ||
var kTrue = exports.kTrue = function kTrue() { | ||
@@ -13,2 +15,6 @@ return true; | ||
function check(value, predicate, error) { | ||
if (!predicate(value)) throw new Error(error); | ||
} | ||
var is = exports.is = { | ||
@@ -26,3 +32,3 @@ undef: function undef(v) { | ||
generator: function generator(g) { | ||
return g.constructor.name === 'GeneratorFunction'; | ||
return is.func(g) && g.constructor.name === 'GeneratorFunction'; | ||
}, | ||
@@ -29,0 +35,0 @@ iterator: function iterator(it) { |
{ | ||
"name": "redux-saga", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"description": "Saga middleware for Redux to handle Side Effects", | ||
@@ -16,3 +16,6 @@ "main": "lib/index.js", | ||
"test-shop": "babel-node examples/shopping-cart/test/sagas.js | tap-spec", | ||
"build-async": "browserify examples/async/src/main.js -t babelify --outfile examples/async/build.js" | ||
"build-async": "browserify examples/async/src/main.js -t babelify --outfile examples/async/build.js", | ||
"build-examples": "npm run build-counter && npm run build-shop && npm run build-async", | ||
"test-examples": "npm run test-counter && npm run test-shop" | ||
}, | ||
@@ -19,0 +22,0 @@ "repository": { |
436
README.md
@@ -9,12 +9,29 @@ An alternative Side Effect model for Redux applications. Instead of dispatching thunks | ||
- Sagas are responsible of orchestrating complex/asynchronous operations (side effects or actions). | ||
This includes simple side effects which react to one action (e.g. send a request on each button click), | ||
but also complex operations that span across multiple actions (e.g. User onBoarding, Wizard | ||
dialogs, asynchronous Game rules ...). | ||
- Sagas are responsible of orchestrating complex/asynchronous operations. | ||
Sagas are created using Generator functions. | ||
A Saga is a generator function that takes user actions as inputs and may yield Side Effects | ||
(e.g. server updates, navigation, store actions ...) as output. | ||
>This middleware is not only about handling asynchronous flow; If all that matters is simplifying | ||
asynchronous control flow, we could simply use async/await with some promise middleware. | ||
What the The middleware provides is | ||
- A composable abstraction **Effect**: Waiting for an action, triggering State updates (by dispatching | ||
actions to the store), calling a remote service are all different forms of Effects. A Saga compose those | ||
Effects using familiar control flow constructs (if, while, for, try/catch). | ||
- The Saga is itself an Effect that can be combined with other Effects using Effect combinators (parallel or | ||
race) and called from inside other Sagas, which all the power of Subroutines and | ||
[Structured Programming](https://en.wikipedia.org/wiki/Structured_programming) | ||
- Effects may be yielded declaratively, i.e. you yield a description of the Effect that | ||
will executed by the middleware. This makes your operational logic inside Generators fully testable. | ||
- You can implement complex operations with logic that spans across multiple actions (e.g. User onBoarding, Wizard | ||
dialogs, complex Game rules ...), which are not trivial to express using redux-thunk or other effects | ||
middlewares that can be only fired from Action Creators. | ||
- [Getting started](#getting-started) | ||
- [How is this different from other asynchronous middlewares](#how-does-it-works) | ||
- [Declarative Effects](#declarative-effects) | ||
@@ -25,2 +42,3 @@ - [Error handling](#error-handling) | ||
- [Composing Sagas](#composing-sagas) | ||
- [Concurrent tasks tasks with fork/join ](#concurrent-tasks-with-forkjoin) | ||
- [Building from sources](#building-from-sources) | ||
@@ -37,4 +55,5 @@ | ||
```javascript | ||
import { take, call } from 'redux-saga' | ||
// sagas/index.js | ||
function* incrementAsync(io) { | ||
function* incrementAsync() { | ||
@@ -44,3 +63,3 @@ while(true) { | ||
// wait for each INCREMENT_ASYNC action | ||
const nextAction = yield io.take(INCREMENT_ASYNC) | ||
const nextAction = yield take(INCREMENT_ASYNC) | ||
@@ -51,3 +70,3 @@ // call delay : Number -> Promise | ||
// dispatch INCREMENT_COUNTER | ||
yield io.put( increment() ) | ||
yield put( increment() ) | ||
} | ||
@@ -76,36 +95,50 @@ | ||
In the above example we created an `incrementAsync` Saga to handle all `INCREMENT_ASYNC` actions. | ||
The Generator function uses `yield io.take(action)` to wait asynchronously for the next action. | ||
The middleware handles action queries from the Saga by pausing the Generator until an action matching | ||
the query happens. Then it resumes the Generator with the matching action. | ||
#How is this different from other asynchronous middlewares | ||
After receiving the wanted action, the Saga triggers a call to `delay(1000)`, which in our example | ||
returns a Promise that will be resolved after 1 second. Again, the middleware will pause the Generator | ||
until the yielded promise is resolved. | ||
In the above example we created an `incrementAsync` Saga. The call `yield take(action)` is a | ||
typical illustration on how Saga works. | ||
After the 1 second delay, the Saga dispatches an `INCREMENT_COUNTER` action using the `io.put(action)` | ||
method. And as for the 2 precedent statements, the Saga is resumed after resolving the result of the | ||
action dispatch (which will happen immediately if the Store's dispatch function returns a normal value, | ||
but may be later if the dispatch result is a Promise). | ||
Typically, actual middlewares handle some Effects triggered by an Action Creator. for example, | ||
redux-thunk handles *thunks* by calling the triggered thunk providing it with `(getState, dispatch)`, | ||
redux-promise handles Promises by dispatching their resolved values, redux-gen handles generators by | ||
dispatching all yielded actions to the store. The common thing is that all those middlewares shares the | ||
same 'call on each action' pattern. They will be called again and again each time an action happens, | ||
i.e. they are scoped by the *root action* which triggered them. | ||
The Generator uses an infinite loop `while(true)` which means it will stay alive for all the application | ||
lifetime. But you can also create Sagas that last only for a limited amount of time. For example, the | ||
following Saga will wait for the first 3 `INCREMENT_COUNTER` actions, triggers a `showCongratulation()` | ||
action and then finishes. | ||
Sagas works differently, they are not fired from within Action Creators but are started with your | ||
application and choose what user actions to watch for. They are a sort of daemon tasks that run in | ||
the background and choose their own logic of progression. In the example above, `incrementAsync` *pulls* | ||
the `INCREMENT_ASYNC` action, the `yield take(action)` is a *blocking call*, which means the Saga | ||
will not progress until it receives a matching action. | ||
After receiving the queried action, the Saga triggers a call to `delay(1000)`, which in our example | ||
returns a Promise that will be resolved after 1 second. Again, this is a blocking call, so the Saga | ||
will wait for 1 second before continuing on (a better way is `io.call(delay, 1000)`, see section | ||
on declarative Effects). | ||
After the 1 second delay, the Saga dispatches an `INCREMENT_COUNTER` action using the `put(action)` | ||
function. Here also, the Saga will wait for the dispatch result. If the dispatch call returns | ||
a normal value, the Saga resumes *immediately*, but if the result value is a Promise then the | ||
Saga will wait until the Promise is resolved (or rejected). | ||
To generalize, waiting for the next action (`yield take(MY_ACTION)`), for a the future result of | ||
a function (`yield delay(1000)`) or for the result of a dispatch (`yield put(myAction())`) are all | ||
different expressions of the same concept: *yielding a side effect*. Basically this is how Sagas | ||
work. | ||
Note also how `incrementAsync` uses an infinite loop `while(true)` which means it will stay alive | ||
for all the application lifetime. You can also create Sagas that last only for a limited amount of | ||
time. For example, the following Saga will wait for the first 3 `INCREMENT_COUNTER` actions, | ||
triggers a `showCongratulation()` action and then finishes. | ||
```javascript | ||
function* onBoarding(io) { | ||
function* onBoarding() { | ||
for(let i = 0; i < 3; i++) | ||
yield io.take(INCREMENT_COUNTER) | ||
yield take(INCREMENT_COUNTER) | ||
yield io.put( showCongratulation() ) | ||
yield put( showCongratulation() ) | ||
} | ||
``` | ||
The basic idea, is that you use the `yield` operator every time you want to trigger a Side Effect. So | ||
to be more accurate, A Saga is a Generator function that yields Side Effects, wait for their results | ||
then resume with the responses. Everything you `yield` is considered an Effect: waiting for an action, | ||
triggering a server request, dispatching an action to the store... | ||
#Declarative Effects | ||
@@ -116,9 +149,9 @@ | ||
```javascript | ||
function* fetchSaga(io) { | ||
function* fetchSaga() { | ||
// returns a Promise that will resolve with the GET response | ||
const products = yield fetch('/products') | ||
// dispatch a RECEIVE_PRODUCTS action | ||
yield io.put( receiveProducts(products) ) | ||
const products = yield fetch('/products') | ||
// dispatch a RECEIVE_PRODUCTS action | ||
yield put( receiveProducts(products) ) | ||
} | ||
@@ -133,5 +166,3 @@ ``` | ||
```javascript | ||
import io from 'redux-saga/io' // exposed for testing purposes | ||
const iterator = fetchSaga(io) | ||
const iterator = fetchSaga() | ||
assert.deepEqual( iterator.next().value, ?? ) // what do we expect ? | ||
@@ -142,7 +173,7 @@ ``` | ||
`fetch('/products')`. Executing the real service during tests is not a viable nor a practical approach, so we have to *mock* the | ||
fetch service, i.e. we'll have to replace the real `fetch` method with a fake one which doesn't actually run the | ||
GET request but only checks that w've called `fetch` with the right arguments (`'/products'` in our case). | ||
fetch service, i.e. we'll have to replace the real `fetch` method with a fake one which doesn't actually run the | ||
GET request but only checks that we've called `fetch` with the right arguments (`'/products'` in our case). | ||
Mocks makes testing more difficult and less reliable. On the other hand, functions which returns simple values are | ||
easier to test, we can use a simple `equal()` to check the result.This is the way to write the most relibale tests. | ||
Mocks makes testing more difficult and less reliable. On the other hand, functions which returns simple values are | ||
easier to test, we can use a simple `equal()` to check the result.This is the way to write the most reliable tests. | ||
@@ -158,3 +189,3 @@ Not convinced ? I encourage you to read this [Eric Elliott' article] | ||
What we need actually, is just to make sure the `fetchSaga` yields a call with the right function and the right | ||
What we need actually, is just to make sure the `fetchSaga` yields a call with the right function and the right | ||
arguments. For this reason, the library provides some declarative ways to yield Side Effects while still making it | ||
@@ -164,11 +195,13 @@ easy to test the Saga logic | ||
```javascript | ||
function* fetchSaga(io) { | ||
const products = yield io.call( fetch, '/products' ) // don't run the effect | ||
import { call } from 'redux-saga' | ||
function* fetchSaga() { | ||
const products = yield call( fetch, '/products' ) // don't run the effect | ||
} | ||
``` | ||
We're using now `io.call(fn, ...args)` function. **The difference from the precedent example is that now we're not | ||
executing the fetch call immedieately, instead, `io.call` creates a description of the effect**. Just as in | ||
Redux you use action creators to create a plain object descibing the action that will get executed by the Store, | ||
`io.call` creates a plain object describing the function call. The redux-saga middleware takes care of executing | ||
We're using now `call(fn, ...args)` function. **The difference from the precedent example is that now we're not | ||
executing the fetch call immediately, instead, `call` creates a description of the effect**. Just as in | ||
Redux you use action creators to create a plain object describing the action that will get executed by the Store, | ||
`io.call` creates a plain object describing the function call. The redux-saga middleware takes care of executing | ||
the function call and resuming the generator with the resolved response. | ||
@@ -180,6 +213,6 @@ | ||
```javascript | ||
import io from 'redux-saga/io' // exposed for testing purposes | ||
import { call } from 'redux-saga' | ||
const iterator = fetchSaga(io) | ||
assert.deepEqual(iterator.next().value, io.call(fetch, '/products') // expects a io.call(...) value | ||
const iterator = fetchSaga() | ||
assert.deepEqual(iterator.next().value, call(fetch, '/products') // expects a call(...) value | ||
``` | ||
@@ -194,17 +227,19 @@ | ||
The `io.call` method is well suited for functions which return Promise results. Another function | ||
`io.cps` can be used to handle Node style functions (e.g. `fn(...args, callback)` where `callback` | ||
The `call` method is well suited for functions which return Promise results. Another function | ||
`cps` can be used to handle Node style functions (e.g. `fn(...args, callback)` where `callback` | ||
is of the form `(error, result) => ()`). For example | ||
```javascript | ||
const content = yield io.cps(readFile, '/path/to/file') | ||
import { cps } from 'redux-saga' | ||
const content = yield cps(readFile, '/path/to/file') | ||
``` | ||
and of course you can test it just like you test io.call | ||
and of course you can test it just like you test call | ||
```javascript | ||
import io from 'redux-saga/io' // exposed for testing purposes | ||
import { cps } from 'redux-saga' | ||
const iterator = fetchSaga(io) | ||
assert.deepEqual(iterator.next().value, io.cps(readFile, '/path/to/file') ) | ||
const iterator = fetchSaga() | ||
assert.deepEqual(iterator.next().value, cps(readFile, '/path/to/file') ) | ||
``` | ||
@@ -218,11 +253,11 @@ | ||
```javascript | ||
function* checkout(io, getState) { | ||
function* checkout(getState) { | ||
while( yield io.take(types.CHECKOUT_REQUEST) ) { | ||
while( yield take(types.CHECKOUT_REQUEST) ) { | ||
try { | ||
const cart = getState().cart | ||
yield io.call(api.buyProducts, cart) | ||
yield io.put(actions.checkoutSuccess(cart)) | ||
yield call(api.buyProducts, cart) | ||
yield put(actions.checkoutSuccess(cart)) | ||
} catch(error) { | ||
yield io.put(actions.checkoutFailure(error)) | ||
yield put(actions.checkoutFailure(error)) | ||
} | ||
@@ -233,2 +268,26 @@ } | ||
Of course you're not forced to handle you API errors inside try/catch blocks, you can also make | ||
your API service return a normal value with some error flag on it | ||
```javascript | ||
function buyProducts(cart) { | ||
return doPost(...) | ||
.then(result => {result}) | ||
.catch(error => {error}) | ||
} | ||
function* checkout(getState) { | ||
while( yield take(types.CHECKOUT_REQUEST) ) { | ||
try { | ||
const cart = getState().cart | ||
const {result, error} = yield call(api.buyProducts, cart) | ||
if(!error) | ||
yield put(actions.checkoutSuccess(result)) | ||
else | ||
yield put(actions.checkoutFailure(error)) | ||
} | ||
} | ||
``` | ||
#Effect Combinators | ||
@@ -241,4 +300,4 @@ | ||
// Wrong, effects will be executed in sequence | ||
const users = yield io.call(fetch, '/users'), | ||
repose = yield io.call(fetch, '/repose') | ||
const users = yield call(fetch, '/users'), | ||
repose = yield call(fetch, '/repose') | ||
``` | ||
@@ -249,86 +308,60 @@ | ||
```javascript | ||
import { call, race } from 'redux-saga' | ||
// correct, effects will get executed in parallel | ||
const [users, repose] = yield [ | ||
io.call(fetch, '/users'), | ||
io.call(fetch, '/repose') | ||
call(fetch, '/users'), | ||
call(fetch, '/repose') | ||
] | ||
``` | ||
When we yield an array of effects, the Generator is paused until all the effects are resolved (or as soon as | ||
one is rejected, just like specified by the `Promise.all` method). | ||
When we yield an array of effects, the Generator is blocked until all the effects are resolved (or as soon as | ||
one is rejected, just like how `Promise.all` behaves). | ||
Sometimes we also need a behavior similar to `Promise.race`:getting the first resolved effect from | ||
multiple ones. The method `io.race` offers a declarative way of triggering a race between effects. | ||
Sometimes when to start multiple tasks in parallel, we don't want to wait for all of them, we just need | ||
to get the *winner*: the first one that resolves (or rejects). The `race` function offers a way of | ||
triggering a race between multiple effects. | ||
The following shows a Saga that triggers a `showCongratulation` message if the user triggers 3 | ||
`INCREMENT_COUNTER` actions with less than 5 seconds between 2 actions. | ||
The following sample shows a Saga that triggers a remote fetch request, and constrain the response with a | ||
1 second timeout. | ||
```javascript | ||
function* onBoarding(io) { | ||
let nbIncrements = 0 | ||
while(nbIncrements < 3) { | ||
// wait for INCREMENT_COUNTER with a timeout of 5 seconds | ||
const winner = yield io.race({ | ||
increment : io.take(INCREMENT_COUNTER), | ||
timeout : io.call(delay, 5000) | ||
function* fetchPostsWithTimeout() { | ||
while( yield take(FETCH_POSTS) ) { | ||
// starts a race between 2 effects | ||
const {posts, timeout} = race({ | ||
posts : call(fetchApi, '/posts'), | ||
timeout : call(delay, 1000) | ||
}) | ||
if(winner.increment) | ||
nbIncrements++ | ||
if(result) | ||
put( actions.receivePosts(posts) ) | ||
else | ||
nbIncrements = 0 | ||
put( actions.timeoutError() ) | ||
} | ||
yield io.put(showCongratulation()) | ||
} | ||
``` | ||
Note the Saga has an internal state, but that has nothing to do with the state inside the Store, | ||
this is a local state to the Saga function used to control the flow of actions. | ||
If by some means you want to view this state (e.g. shows the user progress) then you have | ||
to move it into the store and create a dedicated action/reducer for it | ||
```javascript | ||
function* onBoarding(io, getState) { | ||
while( getState().nbIncrements < 3 ) { | ||
// wait for INCREMENT_COUNTER with a timeout of 5 seconds | ||
const winner = yield io.race({ | ||
increment : io.take(INCREMENT_COUNTER), | ||
timeout : io.call(delay, 5000) | ||
}) | ||
if(winner.increment) | ||
yield io.put( actions.incNbIncrements() ) | ||
else | ||
yield io.put( actions.resetNbIncrements() ) | ||
} | ||
yield io.put(showCongratulation()) | ||
} | ||
``` | ||
#Sequencing Sagas via yield* | ||
You can use the builtin `yield*` operator to compose multiple sagas in a sequential way. | ||
This allows you to sequence your *macro-operations* in a simple procedural style. | ||
This allows you to sequence your *macro-tasks* in a simple procedural style. | ||
```javascript | ||
function* playLevelOne(io, getState) { ... } | ||
function* playLevelOne(getState) { ... } | ||
function* playLevelTwo(io, getState) { ... } | ||
function* playLevelTwo(getState) { ... } | ||
function* playLevelThree(io, getState) { ... } | ||
function* playLevelThree(getState) { ... } | ||
function* game(io, getState) { | ||
function* game(getState) { | ||
const score1 = yield* playLevelOne(io, getState) | ||
io.put(showScore(score1)) | ||
const score1 = yield* playLevelOne(getState) | ||
put(showScore(score1)) | ||
const score2 = yield* playLevelTwo(io, getState) | ||
io.put(showScore(score2)) | ||
const score2 = yield* playLevelTwo(getState) | ||
put(showScore(score2)) | ||
const score3 = yield* playLevelThree(io, getState) | ||
io.put(showScore(score3)) | ||
const score3 = yield* playLevelThree(getState) | ||
put(showScore(score3)) | ||
@@ -338,5 +371,5 @@ } | ||
Note that using `yield*` will cause the JavaScript runtime to *flatten* the whole sequence. | ||
i.e. the resulting iterator (`game()` return value) will yield all values from the nested | ||
iterators. A more powerful alternative is to use the generic middleware composition mechanism. | ||
Note that using `yield*` will cause the JavaScript runtime to *spread* the whole sequence. | ||
The resulting iterator (from `game()`) will yield all values from the nested | ||
iterators. A more powerful alternative is to use the more generic middleware composition mechanism. | ||
@@ -347,23 +380,39 @@ #Composing Sagas | ||
- You'll likely to test nested generators separately. This leads to test duplication because when | ||
- You'll likely want to test nested generators separately. This leads to some duplication because when | ||
testing the main generator you'll have to iterate again on all the nested generators. This | ||
can be achieved by making the nested tests reusable but the tests will still take more time to execute. | ||
can be achieved by reusing the tests for sub-generators inside the main test but this introduce | ||
more boilerplate inside your tests besides the overhead of the repeated execution. All we want is to | ||
test that the main task yields to the correct subtask not actually execute it. | ||
- More importantly, `yield*` allows only for sequential composition of generators, you can only | ||
- More importantly, `yield*` allows only for sequential composition of tasks, you can only | ||
yield* to one generator at a time. But there can be use cases when you want to launch multiple | ||
operations in parallel. You spawn multiple Sagas in the background and resumes when all the spawned | ||
Sagas are done. | ||
tasks in parallel, and wait for them all to terminate in order to progress. | ||
The Saga middleware offers an alternative way of composition using the simple `yield` statement. | ||
When yielding a generator (more accurately an *iterator*) the middleware will convert the resulting | ||
sub-iterator into a promise that will resolve to that sub-iterator return value (or a rejected with an | ||
eventual error thrown from it). | ||
You can simply use `yield subtask()`. When yielding a call to a generator, the Saga | ||
will wait for the generator to terminate before progressing. And resumes then with the | ||
returned value (or throws if an error propagates from the subtask). | ||
This effectively lets you compose nested generators with other effects, like future actions, | ||
timeouts, ... | ||
```javascript | ||
function* fetchPosts() { | ||
yield put( actions.requestPosts() ) | ||
const products = yield call(fetchApi, '/products') | ||
yield put( actions.receivePosts(products) ) | ||
} | ||
function* watchFetch() { | ||
while ( yield take(FETCH_POSTS) ) { | ||
yield call(fetchPosts) // waits for the fetchPosts task to terminate | ||
} | ||
} | ||
``` | ||
In fact, yielding Sagas is no more different than yielding other effects (future actions, timeouts ...). | ||
It means you can combine those Sagas with all the other types of Effects using the effect combinators | ||
(`[...effects]` and `race({...})`). | ||
For example you may want the user finish some game in a limited amount of time | ||
```javascript | ||
function* game(io, getState) { | ||
function* game(getState) { | ||
@@ -373,5 +422,5 @@ let finished | ||
// has to finish in 60 seconds | ||
const {score, timeout} = yield io.race({ | ||
score : play(io, getState), | ||
timeout : delay(60000) | ||
const {score, timeout} = yield race({ | ||
score : call( play, getState), | ||
timeout : call(delay, 60000) | ||
}) | ||
@@ -381,3 +430,3 @@ | ||
finished = true | ||
io.put( showScore(score) ) | ||
put( showScore(score) ) | ||
} | ||
@@ -389,17 +438,102 @@ } | ||
Or you may want to spawn multiple Sagas in parallel to monitor user actions and congratulate | ||
the user when he accomplishes all the desired tasks. | ||
Or you may want to start multiple Sagas in parallel to monitor user actions and congratulate | ||
the user when he accomplished all the desired tasks. | ||
```javascript | ||
function* monitorTask1(io, getState) {...} | ||
... | ||
function* watchTask1() {...} | ||
function* watchTask2() {...} | ||
function* game(io, getState) { | ||
function* game(getState) { | ||
yield [ call(watchTask1), call(watchTask2)] | ||
yield put( showCongratulation() ) | ||
} | ||
``` | ||
#Concurrent tasks with fork/join | ||
const tasks = yield [ monitorTask1(io, getState), ... ] | ||
the `yield` statement causes the generator to pause until the yielded effect has resolved. If you look | ||
closely at this example | ||
yield io.put( showCongratulation() ) | ||
```javascript | ||
function* watchFetch() { | ||
while ( yield take(FETCH_POSTS) ) { | ||
yield put( actions.requestPosts() ) | ||
const posts = yield call(fetchApi, '/posts') // will be blocked here | ||
yield put( actions.receivePosts(posts) ) | ||
} | ||
} | ||
``` | ||
the `watchFetch` generator will wait until `yield call(fetchPosts)` terminates. Imagine that the | ||
`FETCH_POSTS` action is fired from a `Refresh` button. If our application disables the button between | ||
each fetch (no concurrent fecthes) then there is no issue, because we know that no `FETCH_POSTS` action | ||
will occur until we get the response from the `fetchApi` call. | ||
But what if the application allows the user to click on `Refresh` without waiting for the current request | ||
to terminate ? What will happen ? | ||
The following example illustrates a possible sequence of the events | ||
``` | ||
UI watchFetch | ||
-------------------------------------------------------- | ||
FETCH_POSTS.....................call fetchApi........... waiting to resolve | ||
........................................................ | ||
........................................................ | ||
FETCH_POSTS............................................. missed | ||
........................................................ | ||
FETCH_POSTS............................................. missed | ||
................................fetchApi returned....... | ||
........................................................ | ||
``` | ||
When `watchFetch` is blocked on the `fetchApi` call (waiting for the server response), all `FETCH_POSTS` | ||
occurring in between the call and the response are missed. | ||
To express non blocking call, we can use the `fork` function. A possible rewrite of the previous example | ||
with `fork` can be | ||
```javascript | ||
import { fork } from 'redux-saga' | ||
function* fetchPosts() { | ||
yield put( actions.requestPosts() ) | ||
const posts = yield call(fetchApi, '/posts') | ||
yield put( actions.receivePosts(posts) ) | ||
} | ||
function* watchFetch() { | ||
while ( yield take(FETCH_POSTS) ) { | ||
yield fork(fetchPosts) // will not block here | ||
} | ||
} | ||
``` | ||
`fork` accepts function/generator calls as well as simple effects | ||
```javascript | ||
yield fork(func, ...args) // simple async functions (...) -> Promise | ||
yield fork(generator, ...args) // Generator functions | ||
yield fork( put(someActions) ) // Simple effects | ||
``` | ||
The result of `yield fork(api)` will be a *Task descriptor*. To get the | ||
result of a forked Task in a later time, we use the `join` function | ||
```javascript | ||
// non blocking call | ||
const task = yield fork(...) | ||
// ... later | ||
// now a blocking call, will resolve the outcome of task | ||
const result = yield join(...) | ||
``` | ||
You can also ask a Task if it's still running | ||
```javascript | ||
// attention, we don't use yield | ||
const stillRunning = task.isRunning() | ||
``` | ||
#Building from sources | ||
@@ -406,0 +540,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
250162
66
4383
545
1
37
2