redux-saga
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -6,61 +6,50 @@ 'use strict'; | ||
}); | ||
exports.default = sagaMiddleware; | ||
exports.SAGA_NOT_A_GENERATOR_ERROR = undefined; | ||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | ||
var _utils = require('./utils'); | ||
var SAGA_ARGUMENT_ERROR = exports.SAGA_ARGUMENT_ERROR = "Saga must be a Generator function"; | ||
var _io = require('./io'); | ||
function isGenerator(fn) { | ||
return fn.constructor.name === 'GeneratorFunction'; | ||
} | ||
var _io2 = _interopRequireDefault(_io); | ||
function sagaMiddleware(saga) { | ||
if (!isGenerator(saga)) throw new Error(SAGA_ARGUMENT_ERROR); | ||
var _proc = require('./proc'); | ||
return function (_ref) { | ||
var getState = _ref.getState; | ||
var dispatch = _ref.dispatch; | ||
return function (next) { | ||
return function (action) { | ||
var _proc2 = _interopRequireDefault(_proc); | ||
// hit the reducer | ||
var result = next(action); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
// hit the saga | ||
var generator = saga(getState, action); | ||
iterate(generator); | ||
var SAGA_NOT_A_GENERATOR_ERROR = exports.SAGA_NOT_A_GENERATOR_ERROR = "Saga must be a Generator function"; | ||
return result; | ||
exports.default = function () { | ||
for (var _len = arguments.length, sagas = Array(_len), _key = 0; _key < _len; _key++) { | ||
sagas[_key] = arguments[_key]; | ||
} | ||
function iterate(generator) { | ||
return function (_ref) { | ||
var getState = _ref.getState; | ||
var dispatch = _ref.dispatch; | ||
step(); | ||
var cbs = Array(sagas.length); | ||
function step(arg, isError) { | ||
var _ref2 = isError ? generator.throw(arg) : generator.next(arg); | ||
sagas.forEach(function (saga, i) { | ||
if (!_utils.is.generator(saga)) throw new Error(SAGA_NOT_A_GENERATOR_ERROR); | ||
var effect = _ref2.value; | ||
var done = _ref2.done; | ||
(0, _proc2.default)(saga(_io2.default, getState), function (cb) { | ||
cbs[i] = cb; | ||
return function () { | ||
return (0, _utils.remove)(cbs, cb); | ||
}; | ||
}, dispatch); | ||
}); | ||
// retreives next action/effect | ||
if (!done) { | ||
var response = undefined; | ||
if (typeof effect === 'function') { | ||
response = effect(); | ||
} else if (Array.isArray(effect) && typeof effect[0] === 'function') { | ||
response = effect[0].apply(effect, _toConsumableArray(effect.slice(1))); | ||
} else { | ||
response = dispatch(effect); | ||
} | ||
Promise.resolve(response).then(step, function (err) { | ||
return step(err, true); | ||
}); | ||
} | ||
} | ||
} | ||
return function (next) { | ||
return function (action) { | ||
var result = next(action); // hit reducers | ||
cbs.forEach(function (cb) { | ||
return cb(action); | ||
}); | ||
return result; | ||
}; | ||
}; | ||
}; | ||
} | ||
}; |
{ | ||
"name": "redux-saga", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Saga middleware for Redux to handle Side Effects", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
289
README.md
@@ -1,50 +0,57 @@ | ||
# redux-saga | ||
Exploration of an alternative side effect model for Redux applications. | ||
# How does it work | ||
Instead of dispatching thunks which get handled by the redux-thunk middleware. You create *Sagas* | ||
(not sure if the term applies correctly) | ||
An alternative Side Effect model for Redux applications. Instead of dispatching thunks which get handled by the redux-thunk | ||
middleware. You create *Sagas* to gather all your Side Effects logic in a central place. | ||
A Saga is a generator function that takes `(getState, action)` and can yield side effects as well as | ||
other actions. | ||
This means the logic of the application lives in 2 places | ||
Example | ||
- Reducers are responsible of handling state transitions between actions | ||
- 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 accross multiples | ||
actions (e.g. User onBoarding, Wizard dialogs, aynchronous Game rules ...). | ||
A Saga is a generator function that get user actions as inputs and may yield Side Effects (e.g. server updates, navigation ...) | ||
as well as ther actions. | ||
# Usage | ||
## Getting started | ||
Install | ||
``` | ||
npm install redux-saga | ||
``` | ||
Create the Saga (using the counter example from Redux) | ||
```javascript | ||
function* checkout(getState) { | ||
const cart = getState().cart | ||
try { | ||
// yield a side effect : an api call that returns a promise | ||
const cart1 = yield [api.buyProducts, cart] | ||
// sagas/index.js | ||
function* incrementAsync(io) { | ||
// yield an action with the response from the api call | ||
yield actions.checkoutSuccess(cart1) | ||
} catch(error) { | ||
// catch errors from a rejected promise | ||
yield actions.checkoutFailure(error) | ||
} | ||
} | ||
while(true) { | ||
function* getAllProducts() { | ||
// you can also yield thunks | ||
const products = yield () => api.getProducts() | ||
yield actions.receiveProducts(products) | ||
} | ||
// wait for each INCREMENT_ASYNC action | ||
const nextAction = yield io.take(INCREMENT_ASYNC) | ||
export default function* rootSaga(getState, action) { | ||
switch (action.type) { | ||
case types.GET_ALL_PRODUCTS: | ||
yield* getAllProducts(getState) | ||
break | ||
// call delay : Number -> Promise | ||
yield delay(1000) | ||
case types.CHECKOUT_REQUEST: | ||
yield* checkout(getState) | ||
// dispatch INCREMENT_COUNTER | ||
yield io.put( increment() ) | ||
} | ||
} | ||
export default [incrementAsync] | ||
``` | ||
plug redux-saga in the middleware pipeline | ||
Plug redux-saga in the middleware pipeline | ||
```javascript | ||
// store/configureStore.js | ||
import sagas from '../sagas' | ||
const createStoreWithSaga = applyMiddleware( | ||
// ..., | ||
sagaMiddleware(rootSaga) | ||
sagaMiddleware(...sagas) | ||
)(createStore) | ||
@@ -57,56 +64,208 @@ | ||
The difference from redux-thunk, is that the decision of what to dispatch is not scattered throughout | ||
the action creators, but instead centralized in one place that is an integrated part of you domain logic. | ||
In the above example we created an `incrementAsync` Saga to handle all `INCREMENT_ASYNC` actions. The Generator function uses | ||
`yield io.take` 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. | ||
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. | ||
# How does it work | ||
After the 1s 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 redux dispatch function returns a normal value, but maybe later if the dispatch result is a Promise). | ||
- No application logic inside action creators. All action creators are pure factories of raw-data actions | ||
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. | ||
- All the actions hit the reducers; even "asynchronous" ones. All actions hit also the Saga. | ||
```javascript | ||
function* onBoarding(io) { | ||
- Reducers are responsible of transitioning state between actions | ||
for(let i = 0; i < 3; i++) | ||
yield io.take(INCREMENT_COUNTER) | ||
- Sagas are responsible of orchestrating operations (side effects or actions) | ||
yield io.put( showCongratulation() ) | ||
} | ||
``` | ||
- Sagas are generator functions that can yield | ||
- a thunk of the side effet (e.g. `yield () => api.buyProducts(cart)`) | ||
- an array `[fn, ...args]` (e.g. `yield () => [api.buyProducts, cart]`) | ||
- a dispatch item which will get dispatched and handled by a dedicated middleware (e.g. `yield {[API_CALL]: { endpoint: 'getProducts', payload: [cart] }}`) | ||
## Declarative Effects | ||
Sagas don't execute side effects themselves, they *create* the intended side effect. | ||
Then the side effect gets executed later by the appropriate service (either a middleware or a simple function). | ||
Services return Promise to denote their future results. | ||
Sagas Generators can yield Effects in multiple forms. The simplest and most idiomatic is to yield a Promise result | ||
from an asynchronous call ( e.g. `result = yield fetch(url)` ). However, this approach makes testing difficult, because we have | ||
to mock the services that return the Promises (like `fetch` above). For this reason, the library provides some declarative ways | ||
to yield Side Effects while still making it easy to test the Saga logic. | ||
The saga middleware takes the service response and resumes the Saga generator with the resolved response. This way | ||
Sagas can describe complex workflows with a simple synchronous style. And since they are side-effect free, they can | ||
be tested simply by driving the generator function and testing the successive results. | ||
For example, suppose we have this Saga from the shopping cart example | ||
You can get the response returned from services inside your Saga, and use it | ||
to yield further side effects or other actions. If the service responds with a rejected | ||
promise, an exception is thrown inside the generator and can be handled by a normal | ||
`try/catch` block. | ||
```javascript | ||
function* getAllProducts(io) { | ||
while(true) { | ||
// wait for the next GET_ALL_PRODUCTS action | ||
yield io.take(types.GET_ALL_PRODUCTS) | ||
Here the Saga code from the Shopping cart example. Note that Sagas compose using the `yield *` operator. | ||
// fetches the products from the server | ||
const products = yield fetch('/products') | ||
// dispqtch a RECEIVE_PRODUCTS action with the received results | ||
yield io.put( receiveProducts(products) ) | ||
} | ||
} | ||
``` | ||
In order to test the above Generator, we have to mock the `fetch` call, and drive the Generator by successively calling its `next` method. An alternative way, is to use the `io.call(fn, ...args)` function. This function doesn't actually execute the call itself. | ||
Instead it creates an object that describes the desired call. The middleware then handles automatically the yielded result and run | ||
the corresponding call. | ||
```javascript | ||
function* getAllProducts(io) { | ||
while(true) { | ||
... | ||
const products = yield io.call(fetch, '/products') | ||
... | ||
} | ||
} | ||
``` | ||
This allows us to easily test the Generator outside the Redux middleware environement. | ||
```javascript | ||
import test from 'tape' | ||
import io from 'redux-saga/io' // exposed for testing purposes | ||
/* import fetch, getAllProducts, ... */ | ||
test('getProducts Saga test', function (t) { | ||
const iterator = getProductsSaga(io) | ||
let next = generator.next() | ||
t.deepEqual(next.value, io.take(GET_ALL_PRODUCTS), | ||
"must wait for next GET_ALL_PRODUCTS action" | ||
) | ||
next = iterator.next(getAllProducts()) | ||
t.deepEqual(next.value, io.call(fetch, '/products'), | ||
"must yield api.getProducts" | ||
) | ||
next = generator.next(products) | ||
t.deepEqual(next.value, io.put( receiveProducts(products) ), | ||
"must yield actions.receiveProducts(products)" | ||
) | ||
t.end() | ||
}) | ||
``` | ||
# setup and run the examples | ||
All we had to do is to test the successive results returned from the Generator iteration process. When using the declarative forms | ||
Sagas generator functions are effectively nothing more than functions which get a list of successive inputs (via `io.take(pattern)`) and yield a list of successive outputs (via `io.call(fn, ...args)`, `io.put(action)` and other forms we'll see in a moement). | ||
`npm install` | ||
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` is of the form `(error, result) => ()`). For example | ||
There are 2 examples ported from the Redux repos. You can observe the logged actions/effects | ||
into the console (logged via the redux-logger middleware). | ||
```javascript | ||
const content = yield io.cps(readFile, '/path/to/file') | ||
``` | ||
## Error handling | ||
You can catch errors inside the Generator using the simple try/catch syntax. Un the following example, the Saga catch errors | ||
from the `api.buyProducts` call (i.e. a rejected Promise) | ||
```javascript | ||
function* checkout(io, getState) { | ||
while( yield io.take(types.CHECKOUT_REQUEST) ) { | ||
try { | ||
const cart = getState().cart | ||
yield io.call(api.buyProducts, cart) | ||
yield io.put(actions.checkoutSuccess(cart)) | ||
} catch(error) { | ||
yield io.put(actions.checkoutFailure(error)) | ||
} | ||
} | ||
} | ||
``` | ||
## Effect Combinators | ||
The `yield` statements are great for representing asynchronous control flow in a simple and linear style. But we also need to | ||
do things in parallel. We can't simply write | ||
```javascript | ||
// Wrong, effects will be executed in sequence | ||
const users = yield io.call(fetch, '/users'), | ||
repose = yield io.call(fetch, '/repose') | ||
``` | ||
Becaues the 2nd Effect will not get executed until the first call resolves. Instead we have to write | ||
```javascript | ||
// correct, effects will get executed in parallel | ||
const [users, repose] = yield [ | ||
io.call(fetch, '/users'), | ||
io.call(fetch, '/repose') | ||
] | ||
``` | ||
When we yield an array of effects, the Generator is paused until all the effects are resolved (or rejected as we'll see later). | ||
Sometimes we also need a behavior similar to `Promise.race`, i.e. gets the first resolved effect from multiples ones. The method `io.race` offers a declarative way of triggering a race between 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. | ||
```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) | ||
}) | ||
if(winner.increment) | ||
nbIncrements++ | ||
else | ||
nbIncrements = 0 | ||
} | ||
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 controle the flow of actions. | ||
## Generator delegation via yield* | ||
TBD | ||
## Yielding Generators inside Generators | ||
TBD | ||
# Building from sources | ||
``` | ||
git clone https://github.com/yelouafi/redux-saga.git | ||
cd redux-saga | ||
npm install | ||
npm test | ||
``` | ||
There are 2 examples ported from the Redux repos | ||
Counter example | ||
`npm run build-counter` | ||
``` | ||
// build the example | ||
npm run build-counter | ||
// test sample for the generator | ||
npm run test-counter | ||
``` | ||
Shopping Cart example | ||
`npm run build-shop` | ||
``` | ||
// build the example | ||
npm run build-shop | ||
There are also specs samples that test the Saga generators | ||
`npm run test-counter` | ||
`npm run test-shop` | ||
// test sample for the generator | ||
npm run test-shop | ||
``` |
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
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
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
20107
8
225
271
1