redux-saga
Advanced tools
Comparing version 0.3.0 to 0.3.1
@@ -73,3 +73,3 @@ 'use strict'; | ||
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)) ? 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); | ||
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)) ? runPutEffect(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); | ||
} | ||
@@ -83,2 +83,8 @@ | ||
function runPutEffect(action) { | ||
return Promise.resolve(1).then(function () { | ||
return dispatch(action); | ||
}); | ||
} | ||
function runCallEffect(fn, args) { | ||
@@ -85,0 +91,0 @@ return !_utils.is.generator(fn) ? Promise.resolve(fn.apply(undefined, _toConsumableArray(args))) : proc(fn.apply(undefined, _toConsumableArray(args)), subscribe, dispatch); |
{ | ||
"name": "redux-saga", | ||
"version": "0.3.0", | ||
"version": "0.3.1", | ||
"description": "Saga middleware for Redux to handle Side Effects", | ||
@@ -12,7 +12,7 @@ "main": "lib/index.js", | ||
"prepublish": "npm run check && npm run compile", | ||
"build-counter": "browserify examples/counter/src/main.js -t babelify --outfile examples/counter/build.js", | ||
"build-counter": "browserify --debug examples/counter/src/main.js -t babelify --outfile examples/counter/build.js", | ||
"test-counter": "babel-node examples/counter/test/sagas.js | tap-spec", | ||
"build-shop": "browserify examples/shopping-cart/src/main.js -t babelify --outfile examples/shopping-cart/build.js", | ||
"build-shop": "browserify --debug examples/shopping-cart/src/main.js -t babelify --outfile examples/shopping-cart/build.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 --debug examples/async/src/main.js -t babelify --outfile examples/async/build.js", | ||
@@ -19,0 +19,0 @@ "build-examples": "npm run build-counter && npm run build-shop && npm run build-async", |
195
README.md
@@ -13,25 +13,24 @@ An alternative Side Effect model for Redux applications. Instead of dispatching thunks | ||
>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. | ||
>This middleware is not only about handling asynchronous flow. If all what matters is simplifying | ||
asynchronous control flow, one could simply use async/await functions with some promise middleware. | ||
What the The middleware provides is | ||
What this middleware proposes 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 | ||
- 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 composes 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) | ||
- The Saga is itself an Effect. It can be combined with other Effects using combinators. | ||
It can also be called from inside other Sagas, providing the full 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. | ||
- Effects may be yielded declaratively. You yield a description of the Effect which will be | ||
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. | ||
dialogs, complex Game rules ...), which are not trivial to express using other effects middlewares. | ||
- [Getting started](#getting-started) | ||
- [How is this different from other asynchronous middlewares](#how-does-it-works) | ||
- [How is this different from other asynchronous middlewares](#how-is-this-different-from-the-others) | ||
- [Declarative Effects](#declarative-effects) | ||
@@ -42,3 +41,3 @@ - [Error handling](#error-handling) | ||
- [Composing Sagas](#composing-sagas) | ||
- [Concurrent tasks tasks with fork/join ](#concurrent-tasks-with-forkjoin) | ||
- [Non blocking calls with fork/join](#non-blocking-calls-with-forkjoin) | ||
- [Building from sources](#building-from-sources) | ||
@@ -55,4 +54,5 @@ | ||
```javascript | ||
import { take, call } from 'redux-saga' | ||
import { take, put } from 'redux-saga' | ||
// sagas/index.js | ||
function* incrementAsync() { | ||
@@ -65,3 +65,4 @@ | ||
// call delay : Number -> Promise | ||
// delay is a sample function | ||
// return a Promise that resolves after (ms) milliseconds | ||
yield delay(1000) | ||
@@ -94,18 +95,18 @@ | ||
#How is this different from other asynchronous middlewares | ||
#How is this different from the others | ||
In the above example we created an `incrementAsync` Saga. The call `yield take(action)` is a | ||
typical illustration on how Saga works. | ||
In the previous example we created an `incrementAsync` Saga. The call `yield take(action)` is a | ||
typical illustration of how Sagas work. | ||
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 | ||
Typically, actual middlewares handle some Effect form triggered by an Action Creator. For example, | ||
redux-thunk handles *thunks* by calling them with `(getState, dispatch)` as arguments, | ||
redux-promise handles Promises by dispatching their resolved values. redux-gen handles generators by | ||
dispatching all yielded actions to the store. The common thing that all those middlewares share is 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. | ||
i.e. they are *scoped* by the *root action* that triggered them. | ||
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 | ||
Sagas work differently, they are not fired from within Action Creators but are started with your | ||
application and choose what user actions to watch. They are like 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 | ||
the `INCREMENT_ASYNC` action using `yield take(...)`. This is a *blocking call*, which means the Saga | ||
will not progress until it receives a matching action. | ||
@@ -115,18 +116,17 @@ | ||
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). | ||
will wait for 1 second before continuing on (a better way is `call(delay, 1000)`, as we'll see in | ||
the section on declarative Effects). | ||
After the 1 second delay, the Saga dispatches an `INCREMENT_COUNTER` action using the `put(action)` | ||
After the 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 | ||
a normal value, the Saga resumes *immediately* (asap), 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. | ||
To generalize, waiting for a future action (`yield take(MY_ACTION)`), waiting for the future result of | ||
a function call (`yield delay(1000)`) or waiting for the result of a dispatch (`yield put(myAction())`) | ||
all are the same concept. In all cases, we are yielding some form of side effects. | ||
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, | ||
time. For example, the following Saga waits for the first 3 `INCREMENT_COUNTER` actions, | ||
triggers a `showCongratulation()` action and then finishes. | ||
@@ -151,2 +151,3 @@ | ||
// fetch is a sample function | ||
// returns a Promise that will resolve with the GET response | ||
@@ -160,4 +161,4 @@ const products = yield fetch('/products') | ||
In the example above, `fetch('/url')` returns a Promise that will resolve with the GET response. So the 'fetch effect' will be | ||
executed immediately . Simple and idiomatic but ... | ||
In the example above, `fetch('/products')` returns a Promise that will resolve with the GET response. | ||
So the 'fetch effect' will be executed immediately . Simple and idiomatic but ... | ||
@@ -172,7 +173,7 @@ Suppose we want to test generator above | ||
We want to check the result of the first value yielded by the generator, which is in our case the result of running | ||
`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 we've called `fetch` with the right arguments (`'/products'` in our case). | ||
`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 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 | ||
Mocks make testing more difficult and less reliable. On the other hand, functions that simply return 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. | ||
@@ -204,3 +205,3 @@ | ||
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 | ||
`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. | ||
@@ -225,3 +226,3 @@ | ||
The `call` method is well suited for functions which return Promise results. Another function | ||
The `call` method is well suited for functions that return Promise results. Another function | ||
`cps` can be used to handle Node style functions (e.g. `fn(...args, callback)` where `callback` | ||
@@ -300,6 +301,6 @@ is of the form `(error, result) => ()`). For example | ||
Because the 2nd Effect will not get executed until the first call resolves. Instead we have to write | ||
Because the 2nd effect will not get executed until the first call resolves. Instead we have to write | ||
```javascript | ||
import { call, race } from 'redux-saga' | ||
import { call } from 'redux-saga' | ||
@@ -313,6 +314,6 @@ // correct, effects will get executed in parallel | ||
When we yield an array of effects, the Generator is blocked until all the effects are resolved (or as soon as | ||
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 when to start multiple tasks in parallel, we don't want to wait for all of them, we just need | ||
Sometimes we start multiple tasks in parallel but 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 | ||
@@ -325,6 +326,8 @@ triggering a race between multiple effects. | ||
```javascript | ||
import { race, take, put } from 'redux-saga' | ||
function* fetchPostsWithTimeout() { | ||
while( yield take(FETCH_POSTS) ) { | ||
// starts a race between 2 effects | ||
const {posts, timeout} = race({ | ||
const {posts, timeout} = yield race({ | ||
posts : call(fetchApi, '/posts'), | ||
@@ -374,17 +377,14 @@ timeout : call(delay, 1000) | ||
While using `yield*` provides an idiomatic way of compositing Sagas. The approach has some limits: | ||
While using `yield*` provides an idiomatic way of composing Sagas. This approach has some limits: | ||
- 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 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. | ||
- You'll likely want to test nested generators separately. This leads to some duplication in the test | ||
code as well as an overhead of the duplicated execution. We don't want to execute a nested generator | ||
but only make sure the call to it was issued with the right argument. | ||
- 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 | ||
tasks in parallel, and wait for them all to terminate in order to progress. | ||
yield* to one generator at a time. | ||
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). | ||
You can simply use `yield` to start one or more subtasks in parallel. When yielding a call to a | ||
generator, the Saga will wait for the generator to terminate before progressing, then resumes | ||
with the returned value (or throws if an error propagates from the subtask). | ||
@@ -406,5 +406,14 @@ | ||
Yielding to an array of nested generators will start all the sub-generators in parallel and wait | ||
for them to finish. Then resume with all the results | ||
```javascript | ||
function* mainSaga(getState) { | ||
const results = yield [ call(task1), call(task2), ...] | ||
yield put( showResults(results) ) | ||
} | ||
``` | ||
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({...})`). | ||
It means you can combine those Sagas with all the other types using the effect combinators. | ||
@@ -426,3 +435,3 @@ For example you may want the user finish some game in a limited amount of time | ||
finished = true | ||
put( showScore(score) ) | ||
yield put( showScore(score) ) | ||
} | ||
@@ -434,19 +443,7 @@ } | ||
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. | ||
#Non blocking calls with fork/join | ||
```javascript | ||
function* watchTask1() {...} | ||
function* watchTask2() {...} | ||
the `yield` statement causes the generator to pause until the yielded effect resolves or rejects. | ||
If you look closely at this example | ||
function* game(getState) { | ||
yield [ call(watchTask1), call(watchTask2)] | ||
yield put( showCongratulation() ) | ||
} | ||
``` | ||
#Concurrent tasks with fork/join | ||
the `yield` statement causes the generator to pause until the yielded effect has resolved. If you look | ||
closely at this example | ||
```javascript | ||
@@ -456,3 +453,3 @@ function* watchFetch() { | ||
yield put( actions.requestPosts() ) | ||
const posts = yield call(fetchApi, '/posts') // will be blocked here | ||
const posts = yield call(fetchApi, '/posts') // blocking call | ||
yield put( actions.receivePosts(posts) ) | ||
@@ -463,9 +460,9 @@ } | ||
the `watchFetch` generator will wait until `yield call(fetchPosts)` terminates. Imagine that the | ||
the `watchFetch` generator will wait until `yield call(fetchApi, '/posts')` 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 | ||
each fetch (no concurrent fetches) 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 ? | ||
But what happens if the application allows the user to click on `Refresh` without waiting for the | ||
current request to terminate ? | ||
@@ -487,10 +484,10 @@ The following example illustrates a possible sequence of the events | ||
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. | ||
When `watchFetch` is blocked on the `fetchApi` call, 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 | ||
To express non blocking calls, we can use the `fork` function. A possible rewrite of the previous example | ||
with `fork` can be | ||
```javascript | ||
import { fork } from 'redux-saga' | ||
import { fork, call, take, put } from 'redux-saga' | ||
@@ -505,3 +502,3 @@ function* fetchPosts() { | ||
while ( yield take(FETCH_POSTS) ) { | ||
yield fork(fetchPosts) // will not block here | ||
yield fork(fetchPosts) // non blocking call | ||
} | ||
@@ -519,12 +516,14 @@ } | ||
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 | ||
The result of `yield fork(api)` is a *Task descriptor*. To get the result of a forked Task | ||
in a later time, we use the `join` function | ||
```javascript | ||
import { fork, join } from 'redux-saga' | ||
// non blocking call | ||
const task = yield fork(...) | ||
const task = yield fork(subtask, ...args) | ||
// ... later | ||
// now a blocking call, will resolve the outcome of task | ||
const result = yield join(...) | ||
// now a blocking call, will resume with the outcome of task | ||
const result = yield join(task) | ||
``` | ||
@@ -549,3 +548,3 @@ | ||
There are 2 examples ported from the Redux repos | ||
There are 3 examples ported from the Redux repos | ||
@@ -569,1 +568,9 @@ Counter example | ||
``` | ||
async example | ||
``` | ||
// build the example | ||
npm run build-async | ||
//sorry, no tests yet | ||
``` |
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
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
552
0
0
1
35695
8
363