Socket
Socket
Sign inDemoInstall

redux-saga

Package Overview
Dependencies
Maintainers
1
Versions
74
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

redux-saga - npm Package Compare versions

Comparing version 0.1.1 to 0.2.0

lib/io.js

77

lib/index.js

@@ -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",

@@ -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
```
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc