├── .github └── issue_template.md ├── .gitignore ├── .travis.yml ├── README.md ├── SUMMARY.md ├── bin └── deploy.sh ├── book.json ├── docs ├── ExternalResources.md ├── Glossary.md ├── README.md ├── Troubleshooting.md ├── advanced │ ├── Channels.md │ ├── ComposingSagas.md │ ├── Concurrency.md │ ├── ForkModel.md │ ├── FutureActions.md │ ├── NonBlockingCalls.md │ ├── README.md │ ├── RacingEffects.md │ ├── RunningTasksInParallel.md │ ├── SequencingSagas.md │ ├── TaskCancellation.md │ ├── Testing.md │ └── UsingRunSaga.md ├── api │ └── README.md ├── basics │ ├── DeclarativeEffects.md │ ├── DispatchingActions.md │ ├── Effect.md │ ├── ErrorHandling.md │ ├── README.md │ └── UsingSagaHelpers.md ├── introduction │ ├── BeginnerTutorial.md │ ├── README.md │ └── SagaBackground.md └── recipes │ └── README.md ├── docs_kr ├── ExternalResources.md ├── Glossary.md ├── README.md ├── SUMMARY.md ├── Troubleshooting.md ├── advanced │ ├── Channels.md │ ├── ComposingSagas.md │ ├── Concurrency.md │ ├── ForkModel.md │ ├── FutureActions.md │ ├── NonBlockingCalls.md │ ├── README.md │ ├── RacingEffects.md │ ├── RunningTasksInParallel.md │ ├── SequencingSagas.md │ ├── TaskCancellation.md │ ├── Testing.md │ └── UsingRunSaga.md ├── api │ └── README.md ├── basics │ ├── DeclarativeEffects.md │ ├── DispatchingActions.md │ ├── Effect.md │ ├── ErrorHandling.md │ ├── README.md │ └── UsingSagaHelpers.md ├── introduction │ ├── BeginnerTutorial.md │ ├── README.md │ └── SagaBackground.md └── recipes │ └── README.md ├── logo ├── 3840 │ ├── Redux-Saga-Logo-Compact.png │ ├── Redux-Saga-Logo-Landscape.png │ ├── Redux-Saga-Logo-Portrait.png │ └── Redux-Saga-Logo.png ├── 0800 │ ├── Redux-Saga-Logo-Compact.png │ ├── Redux-Saga-Logo-Landscape.png │ ├── Redux-Saga-Logo-Portrait.png │ └── Redux-Saga-Logo.png └── README.md ├── package.json └── transition-progress.md /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | #### Q. 번역 활동에 감사드립니다! 먼저, [전체 진행 상태 문서](https://github.com/mskims/redux-saga-in-korean/blob/master/transition-progress.md) 를 보시고 번역해주실 챕터 를 알려주세요. 2 | 3 | 1. 4 | 5 | 9 | 10 | #### Q. 예상 소요 기간은 어느정도 인가요? 11 | 0 일 12 | 13 | ### 수정하신 후 PR 을 생성해주시면 Merge 하겠습니다. 감사합니다! 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history/ 2 | 3 | .idea/ 4 | .idea/workspace.xml 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "9" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | before_script: 11 | - npm i -g gitbook-cli@2.3.0 12 | - gitbook install 13 | 14 | script: npm run build 15 | 16 | deploy: 17 | provider: script 18 | script: ./bin/deploy.sh 19 | skip_cleanup: true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-saga 한국어 번역 프로젝트 2 | [![Build Status](https://travis-ci.org/mskims/redux-saga-in-korean.svg?branch=master)](https://travis-ci.org/mskims/redux-saga-in-korean) 3 | 4 | [`redux-saga` 저장소](https://github.com/redux-saga/redux-saga)의 의 동의를 구하고 시작한 번역 프로젝트 입니다. 5 | 6 | [`README` 에 저희 저장소가 링크되어 있습니다!](https://github.com/redux-saga/redux-saga/pull/954) 7 | 8 | # 문서 9 | [https://mskims.github.io/redux-saga-in-korean/](https://mskims.github.io/redux-saga-in-korean) 10 | 11 | # 진행상태 12 | [여기](https://github.com/mskims/redux-saga-in-korean/blob/master/transition-progress.md) 를 참조해주세요. 13 | 14 | # 기여 15 | 번역하실 부분을 설명하는 [이슈](https://github.com/mskims/redux-saga-in-korean/issues) 를 생성하시고 작업하신 후 [PR](https://github.com/mskims/redux-saga-in-korean/pulls) 을 보내주세요! 16 | 17 | [docks_kr](https://github.com/mskims/redux-saga-in-korean/tree/master/docs_kr) 의 내용만 번역하시면 됩니다. 18 | 19 | 감사합니다. 20 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | docs/README.md -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # exit with nonzero exit code if anything fails 3 | 4 | # clear and re-create the out directory 5 | rm -rf _book || exit 0; 6 | 7 | # run our compile script, discussed above 8 | npm run build 9 | 10 | # go to the out directory and create a *new* Git repo 11 | cd _book 12 | git init 13 | 14 | # inside this git repo we'll pretend to be a new user 15 | git config user.name "mskims" 16 | git config user.email "its@mskim.me" 17 | 18 | # The first and only commit to this new Git repo contains all the 19 | # files present with the commit message "Deploy to GitHub Pages". 20 | git add . 21 | git commit -m "Deploy to GitHub Pages" 22 | 23 | # Force push from the current repo's master branch to the remote 24 | # repo's gh-pages branch. (All previous history on the gh-pages branch 25 | # will be lost, since we are overwriting it.) We redirect any output to 26 | # /dev/null to hide any sensitive credential data that might otherwise be exposed. 27 | git push --force --quiet "https://${GITHUB_TOKEN}@github.com/mskims/redux-saga-in-korean.git" master:gh-pages 28 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "3.2.0", 3 | "root": "./docs_kr", 4 | "language": "ko", 5 | "plugins": [ 6 | "prism@1.0.0", 7 | "-highlight", 8 | "github", 9 | "ga", 10 | "edit-link" 11 | ], 12 | "pluginsConfig": { 13 | "github": { 14 | "url": "https://github.com/mskims/redux-saga-in-korean/" 15 | }, 16 | "sharing": { 17 | "facebook": true, 18 | "twitter": true, 19 | "google": false, 20 | "weibo": false, 21 | "instapaper": false, 22 | "vk": false 23 | }, 24 | "theme-default": { 25 | "styles": { 26 | "website": "build/gitbook.css" 27 | } 28 | }, 29 | "ga": { 30 | "token": "UA-100043099-1" 31 | }, 32 | "edit-link": { 33 | "base": "https://github.com/mskims/redux-saga-in-korean/tree/master/docs_kr", 34 | "label": "이 페이지 수정하기" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /docs/ExternalResources.md: -------------------------------------------------------------------------------- 1 | # External Resources 2 | 3 | ### Articles on Generators 4 | 5 | - [The Definitive Guide to the JavaScript Generators](http://gajus.com/blog/2/the-definitive-guide-to-the-javascript-generators) by Gajus Kuizinas 6 | - [The Basics Of ES6 Generators](https://davidwalsh.name/es6-generators) by Kyle Simpson 7 | - [ES6 generators in depth](http://www.2ality.com/2015/03/es6-generators.html) by Axel Rauschmayer 8 | 9 | ### Articles on redux-saga 10 | 11 | - [Redux nowadays: From actions creators to sagas](https://riad.blog/2015/12/28/redux-nowadays-from-actions-creators-to-sagas/) by Riad Benguella 12 | - [Managing Side Effects In React + Redux Using Sagas](http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/) by Jack Hsu 13 | - [Using redux-saga To Simplify Your Growing React Native Codebase](https://medium.com/infinite-red/using-redux-saga-to-simplify-your-growing-react-native-codebase-2b8036f650de#.7wl4wr1tk) by Steve Kellock 14 | - [Master Complex Redux Workflows with Sagas](http://konkle.us/master-complex-redux-workflows-with-sagas/) by Brandon Konkle 15 | - [Handling async in Redux with Sagas](http://wecodetheweb.com/2016/01/23/handling-async-in-redux-with-sagas/) by Niels Gerritsen 16 | - [Tips to handle Authentication in Redux](https://medium.com/@MattiaManzati/tips-to-handle-authentication-in-redux-2-introducing-redux-saga-130d6872fbe7#.g49x2gj1g) by Mattia Manzati 17 | - [Build an Image Gallery Using React, Redux and redux-saga](http://joelhooks.com/blog/2016/03/20/build-an-image-gallery-using-redux-saga/?utm_content=bufferbadc3&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer) by Joel Hooks 18 | - [Async Operations using redux saga](https://medium.com/@andresmijares25/async-operations-using-redux-saga-2ba02ae077b3#.556ey5blj) by Andrés Mijares 19 | - [Introduction to Redux Saga](https://ohyayanotherblog.ghost.io/redux-saga-clock/) by Matt Granmoe 20 | - [Vuex meets Redux-saga](https://medium.com/@xanf/vuex-meets-redux-saga-e9c6b46555e#.d4318am40) by Illya Klymov 21 | 22 | ### Addons 23 | - [redux-saga-sc](https://www.npmjs.com/package/redux-saga-sc) – Provides sagas to easily dispatch redux actions over SocketCluster websockets 24 | - [redux-form-saga](https://www.npmjs.com/package/redux-form-saga) – An action creator and saga for integrating Redux Form and Redux Saga 25 | - [redux-electron-enhancer](https://www.npmjs.com/package/redux-electron-enhancer) – Redux store which synchronizes between instances in multiple process 26 | - [eslint-plugin-redux-saga](https://www.npmjs.com/package/eslint-plugin-redux-saga) - ESLint rules that help you to write error free sagas 27 | - [redux-saga-router](https://www.npmjs.com/package/redux-saga-router) - Helper for running sagas in response to route changes. 28 | - [vuex-redux-saga](https://github.com/xanf/vuex-redux-saga) - Bridge between Vuex and Redux-Saga 29 | -------------------------------------------------------------------------------- /docs/Glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | This is a glossary of the core terms in Redux Saga. 4 | 5 | ### Effect 6 | 7 | An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware. 8 | 9 | You create effects using factory functions provided by the redux-saga library. For example you use 10 | `call(myfunc, 'arg1', 'arg2')` to instruct the middleware to invoke `myfunc('arg1', 'arg2')` and return 11 | the result back to the Generator that yielded the effect 12 | 13 | ### Task 14 | 15 | A task is like a process running in background. In a redux-saga based application there can be 16 | multiple tasks running in parallel. You create tasks by using the `fork` function 17 | 18 | ```javascript 19 | function* saga() { 20 | ... 21 | const task = yield fork(otherSaga, ...args) 22 | ... 23 | } 24 | ``` 25 | 26 | ### Blocking/Non-blocking call 27 | 28 | A Blocking call means that the Saga yielded an Effect and will wait for the outcome of its execution before 29 | resuming to the next instruction inside the yielding Generator. 30 | 31 | A Non-blocking call means that the Saga will resume immediately after yielding the Effect. 32 | 33 | For example 34 | 35 | ```javascript 36 | function* saga() { 37 | yield take(ACTION) // Blocking: will wait for the action 38 | yield call(ApiFn, ...args) // Blocking: will wait for ApiFn (If ApiFn returns a Promise) 39 | yield call(otherSaga, ...args) // Blocking: will wait for otherSaga to terminate 40 | 41 | yield put(...) // Non-Blocking: will dispatch within internal scheduler 42 | 43 | const task = yield fork(otherSaga, ...args) // Non-blocking: will not wait for otherSaga 44 | yield cancel(task) // Non-blocking: will resume immediately 45 | // or 46 | yield join(task) // Blocking: will wait for the task to terminate 47 | } 48 | ``` 49 | 50 | ### Watcher/Worker 51 | 52 | refers to a way of organizing the control flow using two separate Sagas 53 | 54 | - The watcher: will watch for dispatched actions and fork a worker on every action 55 | 56 | - The worker: will handle the action and terminate 57 | 58 | example 59 | 60 | ```javascript 61 | function* watcher() { 62 | while (true) { 63 | const action = yield take(ACTION) 64 | yield fork(worker, action.payload) 65 | } 66 | } 67 | 68 | function* worker(payload) { 69 | // ... do some stuff 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | * [Read Me](/README.md) 4 | * [Introduction](/docs/introduction/README.md) 5 | * [Beginner Tutorial](/docs/introduction/BeginnerTutorial.md) 6 | * [Saga background](/docs/introduction/SagaBackground.md) 7 | * [Basic Concepts](/docs/basics/README.md) 8 | * [Using Saga Helpers](/docs/basics/UsingSagaHelpers.md) 9 | * [Declarative Effects](/docs/basics/DeclarativeEffects.md) 10 | * [Dispatching actions](/docs/basics/DispatchingActions.md) 11 | * [Error handling](/docs/basics/ErrorHandling.md) 12 | * [A common abstraction: Effect](/docs/basics/Effect.md) 13 | * [Advanced Concepts](/docs/advanced/README.md) 14 | * [Pulling future actions](/docs/advanced/FutureActions.md) 15 | * [Non blocking calls](/docs/advanced/NonBlockingCalls.md) 16 | * [Running tasks in parallel](/docs/advanced/RunningTasksInParallel.md) 17 | * [Starting a race between multiple Effects](/docs/advanced/RacingEffects.md) 18 | * [Sequencing Sagas using yield*](/docs/advanced/SequencingSagas.md) 19 | * [Composing Sagas](/docs/advanced/ComposingSagas.md) 20 | * [Task cancellation](/docs/advanced/TaskCancellation.md) 21 | * [redux-saga's fork model](/docs/advanced/ForkModel.md) 22 | * [Common Concurrency Patterns](/docs/advanced/Concurrency.md) 23 | * [Examples of Testing Sagas](/docs/advanced/Testing.md) 24 | * [Connecting Sagas to external Input/Output](/docs/advanced/UsingRunSaga.md) 25 | * [Using Channels](/docs/advanced/Channels.md) 26 | * [Recipes](/docs/recipes/README.md) 27 | * [External Resources](/docs/ExternalResources.md) 28 | * [Troubleshooting](/docs/Troubleshooting.md) 29 | * [Glossary](/docs/Glossary.md) 30 | * [API Reference](/docs/api/README.md) 31 | -------------------------------------------------------------------------------- /docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ### App freezes after adding a saga 4 | 5 | Make sure that you `yield` the effects from the generator function. 6 | 7 | Consider this example: 8 | 9 | ```js 10 | import { take } from 'redux-saga/effects' 11 | 12 | function* logActions() { 13 | while (true) { 14 | const action = take() // wrong 15 | console.log(action) 16 | } 17 | } 18 | ``` 19 | 20 | It will put the application into an infinite loop because `take()` only creates a description of the effect. Unless you `yield` it for the middleware to execute, the `while` loop will behave like a regular `while` loop, and freeze your application. 21 | 22 | Adding `yield` will pause the generator and return control to the Redux Saga middleware which will execute the effect. In case of `take()`, Redux Saga will wait for the next action matching the pattern, and only then will resume the generator. 23 | 24 | To fix the example above, simply `yield` the effect returned by `take()`: 25 | 26 | ```js 27 | import { take } from 'redux-saga/effects' 28 | 29 | function* logActions() { 30 | while (true) { 31 | const action = yield take() // correct 32 | console.log(action) 33 | } 34 | } 35 | ``` 36 | 37 | ### My Saga is missing dispatched actions 38 | 39 | Make sure the Saga is not blocked on some effect. When a Saga is waiting for an Effect to 40 | resolve, it will not be able to take dispatched actions until the Effect is resolved. 41 | 42 | For example, consider this example 43 | 44 | ```javascript 45 | function watchRequestActions() { 46 | while (true) { 47 | const {url, params} = yield take('REQUEST') 48 | yield call(handleRequestAction, url, params) // The Saga will block here 49 | } 50 | } 51 | 52 | function handleRequestAction(url, params) { 53 | const response = yield call(someRemoteApi, url, params) 54 | yield put(someAction(response)) 55 | } 56 | ``` 57 | 58 | When `watchRequestActions` performs `yield call(handleRequestAction, url, params)`, it'll wait 59 | for `handleRequestAction` until it terminates an returns before continuing on the next 60 | `yield take`. For example suppose we have this sequence of events 61 | 62 | ``` 63 | UI watchRequestActions handleRequestAction 64 | ----------------------------------------------------------------------------- 65 | .......................take('REQUEST')....................................... 66 | dispatch(REQUEST)......call(handleRequestAction).......call(someRemoteApi)... Wait server resp. 67 | ............................................................................. 68 | ............................................................................. 69 | dispatch(REQUEST)............................................................ Action missed!! 70 | ............................................................................. 71 | ............................................................................. 72 | .......................................................put(someAction)....... 73 | .......................take('REQUEST')....................................... saga is resumed 74 | ``` 75 | 76 | As illustrated above, when a Saga is blocked on a **blocking call** then it will miss 77 | all the actions dispatched in-between. 78 | 79 | To avoid blocking the Saga, you can use a **non-blocking call** using `fork` instead of `call` 80 | 81 | ```javascript 82 | function watchRequestActions() { 83 | while (true) { 84 | const {url, params} = yield take('REQUEST') 85 | yield fork(handleRequestAction, url, params) // The Saga will resume immediately 86 | } 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/advanced/Channels.md: -------------------------------------------------------------------------------- 1 | # Using Channels 2 | 3 | Until now we've used the `take` and `put` effects to communicate with the Redux Store. Channels generalize those Effects to communicate with external event sources or between Sagas themselves. They can also be used to queue specific actions from the Store. 4 | 5 | In this section, we'll see: 6 | 7 | - How to use the `yield actionChannel` Effect to buffer specific actions from the Store. 8 | 9 | - How to use the `eventChannel` factory function to connect `take` Effects to external event sources. 10 | 11 | - How to create a channel using the generic `channel` factory function and use it in `take`/`put` Effects to 12 | communicate between two Sagas. 13 | 14 | ## Using the `actionChannel` Effect 15 | 16 | Let's review the canonical example: 17 | 18 | ```javascript 19 | import { take, fork, ... } from 'redux-saga/effects' 20 | 21 | function* watchRequests() { 22 | while (true) { 23 | const {payload} = yield take('REQUEST') 24 | yield fork(handleRequest, payload) 25 | } 26 | } 27 | 28 | function* handleRequest(payload) { ... } 29 | ``` 30 | 31 | The above example illustrates the typical *watch-and-fork* pattern. The `watchRequests` saga is using `fork` to avoid blocking and thus not missing any action from the store. A `handleRequest` task is created on each `REQUEST` action. So if there are many actions fired at a rapid rate there can be many `handleRequest` tasks executing concurrently. 32 | 33 | Imagine now that our requirement is as follows: we want to process `REQUEST` serially. If we have at any moment four actions, we want to handle the first `REQUEST` action, then only after finishing this action we process the second action and so on... 34 | 35 | So we want to *queue* all non-processed actions, and once we're done with processing the current request, we get the next message from the queue. 36 | 37 | Redux-Saga provides a little helper Effect `actionChannel`, which can handle this for us. Let's see how we can rewrite the previous example with it: 38 | 39 | ```javascript 40 | import { take, actionChannel, call, ... } from 'redux-saga/effects' 41 | 42 | function* watchRequests() { 43 | // 1- Create a channel for request actions 44 | const requestChan = yield actionChannel('REQUEST') 45 | while (true) { 46 | // 2- take from the channel 47 | const {payload} = yield take(requestChan) 48 | // 3- Note that we're using a blocking call 49 | yield call(handleRequest, payload) 50 | } 51 | } 52 | 53 | function* handleRequest(payload) { ... } 54 | ``` 55 | 56 | The first thing is to create the action channel. We use `yield actionChannel(pattern)` where pattern is interpreted using the same rules we mentioned previously with `take(pattern)`. The difference between the 2 forms is that `actionChannel` **can buffer incoming messages** if the Saga is not yet ready to take them (e.g. blocked on an API call). 57 | 58 | Next is the `yield take(requestChan)`. Besides usage with a `pattern` to take specific actions from the Redux Store, `take` can also be used with channels (above we created a channel object from specific Redux actions). The `take` will block the Saga until a message is available on the channel. The take may also resume immediately if there is a message stored in the underlying buffer. 59 | 60 | The important thing to note is how we're using a blocking `call`. The Saga will remain blocked until `call(handleRequest)` returns. But meanwhile, if other `REQUEST` actions are dispatched while the Saga is still blocked, they will queued internally by `requestChan`. When the Saga resumes from `call(handleRequest)` and executes the next `yield take(requestChan)`, the take will resolve with the queued message. 61 | 62 | By default, `actionChannel` buffers all incoming messages without limit. If you want a more control over the buffering, you can supply a Buffer argument to the effect creator. Redux-Saga provides some common buffers (none, dropping, sliding) but you can also supply your own buffer implementation. [See API docs](../api#buffers) for more details. 63 | 64 | For example if you want to handle only the most recent five items you can use: 65 | 66 | ```javascript 67 | import { buffers } from 'redux-saga' 68 | import { actionChannel } from 'redux-saga/effects' 69 | 70 | function* watchRequests() { 71 | const requestChan = yield actionChannel('REQUEST', buffers.sliding(5)) 72 | ... 73 | } 74 | ``` 75 | 76 | ## Using the `eventChannel` factory to connect to external events 77 | 78 | Like `actionChannel` (Effect), `eventChannel` (a factory function, not an Effect) creates a Channel for events but from event sources other than the Redux Store. 79 | 80 | This simple example creates a Channel from an interval: 81 | 82 | ```javascript 83 | import { eventChannel, END } from 'redux-saga' 84 | 85 | function countdown(secs) { 86 | return eventChannel(emitter => { 87 | const iv = setInterval(() => { 88 | secs -= 1 89 | if (secs > 0) { 90 | emitter(secs) 91 | } else { 92 | // this causes the channel to close 93 | emitter(END) 94 | } 95 | }, 1000); 96 | // The subscriber must return an unsubscribe function 97 | return () => { 98 | clearInterval(iv) 99 | } 100 | } 101 | ) 102 | } 103 | ``` 104 | 105 | The first argument in `eventChannel` is a *subscriber* function. The role of the subscriber is to initialize the external event source (above using `setInterval`), then routes all incoming events from the source to the channel by invoking the supplied `emitter`. In the above example we're invoking `emitter` on each second. 106 | 107 | > Note: You need to sanitize your event sources as to not pass null or undefined through the event channel. While it's fine to pass numbers through, we'd recommend structuring your event channel data like your redux actions. `{ number }` over `number`. 108 | 109 | Note also the invocation `emitter(END)`. We use this to notify any channel consumer that the channel has been closed, meaning no other messages will come through this channel. 110 | 111 | Let's see how we can use this channel from our Saga. (This is taken from the cancellable-counter example in the repo.) 112 | 113 | ```javascript 114 | import { take, put, call } from 'redux-saga/effects' 115 | import { eventChannel, END } from 'redux-saga' 116 | 117 | // creates an event Channel from an interval of seconds 118 | function countdown(seconds) { ... } 119 | 120 | export function* saga() { 121 | const chan = yield call(countdown, value) 122 | try { 123 | while (true) { 124 | // take(END) will cause the saga to terminate by jumping to the finally block 125 | let seconds = yield take(chan) 126 | console.log(`countdown: ${seconds}`) 127 | } 128 | } finally { 129 | console.log('countdown terminated') 130 | } 131 | } 132 | ``` 133 | 134 | So the Saga is yielding a `take(chan)`. This causes the Saga to block until a message is put on the channel. In our example above, it corresponds to when we invoke `emitter(secs)`. Note also we're executing the whole `while (true) {...}` loop inside a `try/finally` block. When the interval terminates, the countdown function closes the event channel by invoking `emitter(END)`. Closing a channel has the effect of terminating all Sagas blocked on a `take` from that channel. In our example, terminating the Saga will cause it to jump to its `finally` block (if provided, otherwise the Saga simply terminates). 135 | 136 | The subscriber returns an `unsubscribe` function. This is used by the channel to unsubscribe before the event source complete. Inside a Saga consuming messages from an event channel, if we want to *exit early* before the event source complete (e.g. Saga has been cancelled) you can call `chan.close()` to close the channel and unsubscribe from the source. 137 | 138 | For example, we can make our Saga support cancellation: 139 | 140 | ```javascript 141 | import { take, put, call, cancelled } from 'redux-saga/effects' 142 | import { eventChannel, END } from 'redux-saga' 143 | 144 | // creates an event Channel from an interval of seconds 145 | function countdown(seconds) { ... } 146 | 147 | export function* saga() { 148 | const chan = yield call(countdown, value) 149 | try { 150 | while (true) { 151 | let seconds = yield take(chan) 152 | console.log(`countdown: ${seconds}`) 153 | } 154 | } finally { 155 | if (yield cancelled()) { 156 | chan.close() 157 | console.log('countdown cancelled') 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | Here is another example of how you can use event channels to pass WebSocket events into your saga (e.g.: using socket.io library). 164 | Suppose you are waiting for a server message `ping` then reply with a `pong` message after some delay. 165 | 166 | 167 | ```javascript 168 | import { take, put, call, apply } from 'redux-saga/effects' 169 | import { eventChannel, delay } from 'redux-saga' 170 | import { createWebSocketConnection } from './socketConnection' 171 | 172 | // this function creates an event channel from a given socket 173 | // Setup subscription to incoming `ping` events 174 | function createSocketChannel(socket) { 175 | // `eventChannel` takes a subscriber function 176 | // the subscriber function takes an `emit` argument to put messages onto the channel 177 | return eventChannel(emit => { 178 | 179 | const pingHandler = (event) => { 180 | // puts event payload into the channel 181 | // this allows a Saga to take this payload from the returned channel 182 | emit(event.payload) 183 | } 184 | 185 | // setup the subscription 186 | socket.on('ping', pingHandler) 187 | 188 | // the subscriber must return an unsubscribe function 189 | // this will be invoked when the saga calls `channel.close` method 190 | const unsubscribe = () => { 191 | socket.off('ping', pingHandler) 192 | } 193 | 194 | return unsubscribe 195 | }) 196 | } 197 | 198 | // reply with a `pong` message by invoking `socket.emit('pong')` 199 | function* pong(socket) { 200 | yield call(delay, 5000) 201 | yield apply(socket, socket.emit, ['pong']) // call `emit` as a method with `socket` as context 202 | } 203 | 204 | export function* watchOnPings() { 205 | const socket = yield call(createWebSocketConnection) 206 | const socketChannel = yield call(createSocketChannel, socket) 207 | 208 | while (true) { 209 | const payload = yield take(socketChannel) 210 | yield put({ type: INCOMING_PONG_PAYLOAD, payload }) 211 | yield fork(pong, socket) 212 | } 213 | } 214 | ``` 215 | 216 | > Note: messages on an eventChannel are not buffered by default. You have to provide a buffer to the eventChannel factory in order to specify buffering strategy for the channel (e.g. `eventChannel(subscriber, buffer)`). 217 | [See the API docs](../api#buffers) for more info. 218 | 219 | ### Using channels to communicate between Sagas 220 | 221 | Besides action channels and event channels. You can also directly create channels which are not connected to any source by default. You can then manually `put` on the channel. This is handy when you want to use a channel to communicate between sagas. 222 | 223 | To illustrate, let's review the former example of request handling. 224 | 225 | ```javascript 226 | import { take, fork, ... } from 'redux-saga/effects' 227 | 228 | function* watchRequests() { 229 | while (true) { 230 | const {payload} = yield take('REQUEST') 231 | yield fork(handleRequest, payload) 232 | } 233 | } 234 | 235 | function* handleRequest(payload) { ... } 236 | ``` 237 | 238 | We saw that the watch-and-fork pattern allows handling multiple requests simultaneously, without limit on the number of worker tasks executing concurrently. Then, we used the `actionChannel` effect to limit the concurrency to one task at a time. 239 | 240 | So let's say that our requirement is to have a maximum of three tasks executing at the same time. When we get a request and there are less than three tasks executing, we process the request immediately, otherwise we queue the task and wait for one of the three *slots* to become free. 241 | 242 | Below is an example of a solution using channels: 243 | 244 | ```javascript 245 | import { channel } from 'redux-saga' 246 | import { take, fork, ... } from 'redux-saga/effects' 247 | 248 | function* watchRequests() { 249 | // create a channel to queue incoming requests 250 | const chan = yield call(channel) 251 | 252 | // create 3 worker 'threads' 253 | for (var i = 0; i < 3; i++) { 254 | yield fork(handleRequest, chan) 255 | } 256 | 257 | while (true) { 258 | const {payload} = yield take('REQUEST') 259 | yield put(chan, payload) 260 | } 261 | } 262 | 263 | function* handleRequest(chan) { 264 | while (true) { 265 | const payload = yield take(chan) 266 | // process the request 267 | } 268 | } 269 | ``` 270 | 271 | In the above example, we create a channel using the `channel` factory. We get back a channel which by default buffers all messages we put on it (unless there is a pending taker, in which the taker is resumed immediately with the message). 272 | 273 | The `watchRequests` saga then forks three worker sagas. Note the created channel is supplied to all forked sagas. `watchRequests` will use this channel to *dispatch* work to the three worker sagas. On each `REQUEST` action the Saga will simply put the payload on the channel. The payload will then be taken by any *free* worker. Otherwise it will be queued by the channel until a worker Saga is ready to take it. 274 | 275 | All the three workers run a typical while loop. On each iteration, a worker will take the next request, or will block until a message is available. Note that this mechanism provides an automatic load-balancing between the 3 workers. Rapid workers are not slowed down by slow workers. 276 | -------------------------------------------------------------------------------- /docs/advanced/ComposingSagas.md: -------------------------------------------------------------------------------- 1 | # Composing Sagas 2 | 3 | While using `yield*` provides an idiomatic way of composing Sagas, this approach has some limitations: 4 | 5 | - You'll likely want to test nested generators separately. This leads to some duplication in the test code as well as the 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. 6 | 7 | - More importantly, `yield*` allows only for sequential composition of tasks, so you can only `yield*` to one generator at a time. 8 | 9 | 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 resume with the returned value (or throws if an error propagates from the subtask). 10 | 11 | ```javascript 12 | function* fetchPosts() { 13 | yield put(actions.requestPosts()) 14 | const products = yield call(fetchApi, '/products') 15 | yield put(actions.receivePosts(products)) 16 | } 17 | 18 | function* watchFetch() { 19 | while (yield take(FETCH_POSTS)) { 20 | yield call(fetchPosts) // waits for the fetchPosts task to terminate 21 | } 22 | } 23 | ``` 24 | 25 | Yielding to an array of nested generators will start all the sub-generators in parallel, wait 26 | for them to finish, then resume with all the results 27 | 28 | ```javascript 29 | function* mainSaga(getState) { 30 | const results = yield [call(task1), call(task2), ...] 31 | yield put(showResults(results)) 32 | } 33 | ``` 34 | 35 | In fact, yielding Sagas is no different than yielding other effects (future actions, timeouts, etc). This means you can combine those Sagas with all the other types using the effect combinators. 36 | 37 | For example, you may want the user to finish some game in a limited amount of time: 38 | 39 | ```javascript 40 | function* game(getState) { 41 | let finished 42 | while (!finished) { 43 | // has to finish in 60 seconds 44 | const {score, timeout} = yield race({ 45 | score: call(play, getState), 46 | timeout: call(delay, 60000) 47 | }) 48 | 49 | if (!timeout) { 50 | finished = true 51 | yield put(showScore(score)) 52 | } 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/advanced/Concurrency.md: -------------------------------------------------------------------------------- 1 | # Concurrency 2 | 3 | In the basics section, we saw how to use the helper effects `takeEvery` and `takeLatest` in order to manage concurrency between Effects. 4 | 5 | In this section we'll see how those helpers could be implemented using the low-level Effects. 6 | 7 | ## `takeEvery` 8 | 9 | ```javascript 10 | function* takeEvery(pattern, saga, ...args) { 11 | const task = yield fork(function* () { 12 | while (true) { 13 | const action = yield take(pattern) 14 | yield fork(saga, ...args.concat(action)) 15 | } 16 | }) 17 | return task 18 | } 19 | ``` 20 | 21 | `takeEvery` allows multiple `saga` tasks to be forked concurrently. 22 | 23 | ## `takeLatest` 24 | 25 | ```javascript 26 | function* takeLatest(pattern, saga, ...args) { 27 | const task = yield fork(function* () { 28 | let lastTask 29 | while (true) { 30 | const action = yield take(pattern) 31 | if (lastTask) 32 | yield cancel(lastTask) // cancel is no-op if the task has already terminated 33 | 34 | lastTask = yield fork(saga, ...args.concat(action)) 35 | } 36 | }) 37 | return task 38 | } 39 | ``` 40 | 41 | `takeLatest` doesn't allow multiple Saga tasks to be fired concurrently. As soon as it gets a new dispatched action, it cancels any previously-forked task (if still running). 42 | 43 | `takeLatest` can be useful to handle AJAX requests where we want to only have the response to the latest request. 44 | -------------------------------------------------------------------------------- /docs/advanced/ForkModel.md: -------------------------------------------------------------------------------- 1 | # redux-saga's fork model 2 | 3 | In `redux-saga` you can dynamically fork tasks that execute in the background using 2 Effects 4 | 5 | - `fork` is used to create *attached forks* 6 | - `spawn` is used to create *detached forks* 7 | 8 | ## Attached forks (using `fork`) 9 | 10 | Attached forks remains attached to their parent by the following rules 11 | 12 | ### Completion 13 | 14 | - A Saga terminates only after 15 | - It terminates its own body of instructions 16 | - All attached forks are themselves terminated 17 | 18 | For example say we have the following 19 | 20 | ```js 21 | import { delay } from 'redux-saga' 22 | import { fork, call, put } from 'redux-saga/effects' 23 | import api from './somewhere/api' // app specific 24 | import { receiveData } from './somewhere/actions' // app specific 25 | 26 | function* fetchAll() { 27 | const task1 = yield fork(fetchResource, 'users') 28 | const task2 = yield fork(fetchResource, 'comments') 29 | yield call(delay, 1000) 30 | } 31 | 32 | function* fetchResource(resource) { 33 | const {data} = yield call(api.fetch, resource) 34 | yield put(receiveData(data)) 35 | } 36 | 37 | function* main() { 38 | yield call(fetchAll) 39 | } 40 | ``` 41 | 42 | `call(fetchAll)` will terminate after: 43 | 44 | - The `fetchAll` body itself terminates, this means all 3 effects are performed. Since `fork` effects are non blocking, the 45 | task will block on `call(delay, 1000)` 46 | 47 | - The 2 forked tasks terminate, i.e. after fetching the required resources and putting the corresponding `receiveData` actions 48 | 49 | So the whole task will block until a delay of 1000 millisecond passed *and* both `task1` and `task2` finished their business. 50 | 51 | Say for example, the delay of 1000 milliseconds elapsed and the 2 tasks hasn't yet finished, then `fetchAll` will still wait 52 | for all forked tasks to finish before terminating the whole task. 53 | 54 | The attentive reader might have noticed the `fetchAll` saga could be rewritten using the parallel Effect 55 | 56 | ```js 57 | function* fetchAll() { 58 | yield [ 59 | call(fetchResource, 'users'), // task1 60 | call(fetchResource, 'comments'), // task2, 61 | call(delay, 1000) 62 | ] 63 | } 64 | ``` 65 | 66 | In fact, attached forks shares the same semantics with the parallel Effect: 67 | 68 | - We're executing tasks in parallel 69 | - The parent will terminate after all launched tasks terminate 70 | 71 | 72 | And this applies for all other semantics as well (error and cancellation propagation). You can understand how 73 | attached forks behave by simply considering it as a *dynamic parallel* Effect. 74 | 75 | ## Error propagation 76 | 77 | Following the same analogy, Let's examine in detail how errors are handled in parallel Effects 78 | 79 | for example, let's say we have this Effect 80 | 81 | ```js 82 | yield [ 83 | call(fetchResource, 'users'), 84 | call(fetchResource, 'comments'), 85 | call(delay, 1000) 86 | ] 87 | ``` 88 | 89 | The above effect will fail as soon as any one of the 3 child Effects fails. Furthermore, the uncaught error will cause 90 | the parallel Effect to cancel all the other pending Effects. So for example if `call(fetchResource), 'users')` raises an 91 | uncaught error, the parallel Effect will cancel the 2 other tasks (if they are still pending) then aborts itself with the 92 | same error from the failed call. 93 | 94 | Similarly for attached forks, a Saga aborts as soon as 95 | 96 | - Its main body of instructions throws an error 97 | 98 | - An uncaught error was raised by one of its attached forks 99 | 100 | So in the previous example 101 | 102 | ```js 103 | //... imports 104 | 105 | function* fetchAll() { 106 | const task1 = yield fork(fetchResource, 'users') 107 | const task2 = yield fork(fetchResource, 'comments') 108 | yield call(delay, 1000) 109 | } 110 | 111 | function* fetchResource(resource) { 112 | const {data} = yield call(api.fetch, resource) 113 | yield put(receiveData(data)) 114 | } 115 | 116 | function* main() { 117 | try { 118 | yield call(fetchAll) 119 | } catch (e) { 120 | // handle fetchAll errors 121 | } 122 | } 123 | ``` 124 | 125 | If at a moment, for example, `fetchAll` is blocked on the `call(delay, 1000)` Effect, and say, `task1` failed, then the whole 126 | `fetchAll` task will fail causing 127 | 128 | - Cancellation of all other pending tasks. This includes: 129 | - The *main task* (the body of `fetchAll`): cancelling it means cancelling the current Effect `call(delay, 1000)` 130 | - The other forked tasks which are still pending. i.e. `task2` in our example. 131 | 132 | - The `call(fetchAll)` will raise itself an error which will be caught in the `catch` body of `main` 133 | 134 | Note we're able to catch the error from `call(fetchAll)` inside `main` only because we're using a blocking call. And that 135 | we can't catch the error directly from `fetchAll`. This a rule of thumb, **you can't catch errors from forked tasks**. A failure 136 | in an attached fork will cause the forking parent to abort (Just like there is no way to catch an error *inside* a parallel Effect, only from 137 | outside by blocking on the parallel Effect). 138 | 139 | 140 | ## Cancellation 141 | 142 | Cancelling a Saga causes the cancellation of: 143 | 144 | - The *main task* this means cancelling the current Effect where the Saga is blocked 145 | 146 | - All attached forks that are still executing 147 | 148 | 149 | **WIP** 150 | 151 | ## Detached forks (using `spawn`) 152 | 153 | Detached forks live in their own execution context. A parent doesn't wait for detached forks to terminate. Uncaught 154 | errors from spawned tasks are not bubbled up to the parent. And cancelling a parent doesn't automatically cancel detached 155 | forks (you need to cancel them explicitly). 156 | 157 | In short, detached forks behave like root Sagas started directly using the `middleware.run` API. 158 | 159 | 160 | **WIP** 161 | -------------------------------------------------------------------------------- /docs/advanced/FutureActions.md: -------------------------------------------------------------------------------- 1 | # Pulling future actions 2 | 3 | Until now we've used the helper effect `takeEvery` in order to spawn a new task on each incoming action. This mimics somewhat the behavior of redux-thunk: each time a Component, for example, invokes a `fetchProducts` Action Creator, the Action Creator will dispatch a thunk to execute the control flow. 4 | 5 | In reality, `takeEvery` is just a wrapper effect for internal helper function built on top of the lower level and more powerful API. In this section we'll see a new Effect, `take`, which makes it possible to build complex control flow by allowing total control of the action observation process. 6 | 7 | ## A simple logger 8 | 9 | Let's take a simple example of a Saga that watches all actions dispatched to the store and logs them to the console. 10 | 11 | Using `takeEvery('*')` (with the wildcard `*` pattern) we can catch all dispatched actions regardless of their types. 12 | 13 | ```javascript 14 | import { select, takeEvery } from 'redux-saga/effects' 15 | 16 | function* watchAndLog() { 17 | yield takeEvery('*', function* logger(action) { 18 | const state = yield select() 19 | 20 | console.log('action', action) 21 | console.log('state after', state) 22 | }) 23 | } 24 | ``` 25 | 26 | Now let's see how to use the `take` Effect to implement the same flow as above 27 | 28 | ```javascript 29 | import { select, take } from 'redux-saga/effects' 30 | 31 | function* watchAndLog() { 32 | while (true) { 33 | const action = yield take('*') 34 | const state = yield select() 35 | 36 | console.log('action', action) 37 | console.log('state after', state) 38 | } 39 | } 40 | ``` 41 | 42 | The `take` is just like `call` and `put` we saw earlier. It creates another command object that tells the middleware to wait for a specific action. The resulting behavior of the `call` Effect is the same as when the middleware suspends the Generator until a Promise resolves. In the `take` case it'll suspend the Generator until a matching action is dispatched. In the above example `watchAndLog` is suspended until any action is dispatched. 43 | 44 | Note how we're running an endless loop `while (true)`. Remember this is a Generator function, which doesn't have a run-to-completion behavior. Our Generator will block on each iteration waiting for an action to happen. 45 | 46 | Using `take` has a subtle impact on how we write our code. In the case of `takeEvery` the invoked tasks have no control on when they'll be called. They will be invoked again and again on each matching action. They also have no control on when to stop the observation. 47 | 48 | In the case of `take` the control is inverted. Instead of the actions being *pushed* to the handler tasks, the Saga is *pulling* the action by itself. It looks as if the Saga is performing a normal function call `action = getNextAction()` which will resolve when the action is dispatched. 49 | 50 | This inversion of control allows us to implement control flows that are non-trivial to do with the traditional *push* approach. 51 | 52 | As a simple example, suppose that in our Todo application, we want to watch user actions and show a congratulation message after the user has created his first three todos. 53 | 54 | ```javascript 55 | import { take, put } from 'redux-saga/effects' 56 | 57 | function* watchFirstThreeTodosCreation() { 58 | for (let i = 0; i < 3; i++) { 59 | const action = yield take('TODO_CREATED') 60 | } 61 | yield put({type: 'SHOW_CONGRATULATION'}) 62 | } 63 | ``` 64 | 65 | Instead of a `while (true)` we're running a `for` loop which will iterate only three times. After taking the first three `TODO_CREATED` actions, `watchFirstThreeTodosCreation` will cause the application to display a congratulation message then terminate. This means the Generator will be garbage collected and no more observation will take place. 66 | 67 | Another benefit of the pull approach is that we can describe our control flow using a familiar synchronous style. For example, suppose we want to implement a login flow with 2 actions `LOGIN` and `LOGOUT`. Using `takeEvery` (or `redux-thunk`) we'll have to write 2 separate tasks (or thunks): one for `LOGIN` and the other for `LOGOUT`. 68 | 69 | The result is that our logic is now spread in 2 places. In order for someone reading our code to understand what's going on, he has to read the source of the 2 handlers and make the link between the logic in both. It means he has to rebuild the model of the flow in his head by rearranging mentally the logic placed in various places of the code in the correct order. 70 | 71 | Using the pull model we can write our flow in the same place instead of handling the same action repeatedly. 72 | 73 | ```javascript 74 | function* loginFlow() { 75 | while (true) { 76 | yield take('LOGIN') 77 | // ... perform the login logic 78 | yield take('LOGOUT') 79 | // ... perform the logout logic 80 | } 81 | } 82 | ``` 83 | 84 | The `loginFlow` Saga more clearly conveys the expected action sequence. It knows that the `LOGIN` action should always be followed by a `LOGOUT` action and that `LOGOUT` is always followed by a `LOGIN` (a good UI should always enforce a consistent order of the actions, by hiding or disabling unexpected action). 85 | -------------------------------------------------------------------------------- /docs/advanced/NonBlockingCalls.md: -------------------------------------------------------------------------------- 1 | # Non-blocking calls 2 | 3 | In the previous section, we saw how the `take` Effect allows us to better describe a non-trivial flow in a central place. 4 | 5 | Revisiting the login flow example: 6 | 7 | ```javascript 8 | function* loginFlow() { 9 | while (true) { 10 | yield take('LOGIN') 11 | // ... perform the login logic 12 | yield take('LOGOUT') 13 | // ... perform the logout logic 14 | } 15 | } 16 | ``` 17 | 18 | Let's complete the example and implement the actual login/logout logic. Suppose we have an API which permits us to authorize the user on a remote server. If the authorization is successful, the server will return an authorization token which will be stored by our application using DOM storage (assume our API provides another service for DOM storage). 19 | 20 | When the user logs out, we'll simply delete the authorization token stored previously. 21 | 22 | ### First try 23 | 24 | So far we have all needed Effects in order to implement the above flow. We can wait for specific actions in the store using the `take` Effect. We can make asynchronous calls using the `call` Effect. Finally, we can dispatch actions to the store using the `put` Effect. 25 | 26 | So let's give it a try: 27 | 28 | > Note: the code below has a subtle issue. Make sure to read the section until the end. 29 | 30 | ```javascript 31 | import { take, call, put } from 'redux-saga/effects' 32 | import Api from '...' 33 | 34 | function* authorize(user, password) { 35 | try { 36 | const token = yield call(Api.authorize, user, password) 37 | yield put({type: 'LOGIN_SUCCESS', token}) 38 | return token 39 | } catch(error) { 40 | yield put({type: 'LOGIN_ERROR', error}) 41 | } 42 | } 43 | 44 | function* loginFlow() { 45 | while (true) { 46 | const {user, password} = yield take('LOGIN_REQUEST') 47 | const token = yield call(authorize, user, password) 48 | if (token) { 49 | yield call(Api.storeItem, {token}) 50 | yield take('LOGOUT') 51 | yield call(Api.clearItem, 'token') 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | First we created a separate Generator `authorize` which will perform the actual API call and notify the Store upon success. 58 | 59 | The `loginFlow` implements its entire flow inside a `while (true)` loop, which means once we reach the last step in the flow (`LOGOUT`) we start a new iteration by waiting for a new `LOGIN_REQUEST` action. 60 | 61 | `loginFlow` first waits for a `LOGIN_REQUEST` action. Then retrieves the credentials in the action payload (`user` and `password`) and makes a `call` to the `authorize` task. 62 | 63 | As you noted, `call` isn't only for invoking functions returning Promises. We can also use it to invoke other Generator functions. In the above example, **`loginFlow` will wait for authorize until it terminates and returns** (i.e. after performing the api call, dispatching the action and then returning the token to `loginFlow`). 64 | 65 | If the API call succeeds, `authorize` will dispatch a `LOGIN_SUCCESS` action then return the fetched token. If it results in an error, it'll dispatch a `LOGIN_ERROR` action. 66 | 67 | If the call to `authorize` is successful, `loginFlow` will store the returned token in the DOM storage and wait for a `LOGOUT` action. When the user logouts, we remove the stored token and wait for a new user login. 68 | 69 | In the case of `authorize` failed, it'll return an undefined value, which will cause `loginFlow` to skip the previous process and wait for a new `LOGIN_REQUEST` action. 70 | 71 | Observe how the entire logic is stored in one place. A new developer reading our code doesn't have to travel between various places in order to understand the control flow. It's like reading a synchronous algorithm: steps are laid out in their natural order. And we have functions which call other functions and wait for their results. 72 | 73 | ### But there is still a subtle issue with the above approach 74 | 75 | Suppose that when the `loginFlow` is waiting for the following call to resolve: 76 | 77 | ```javascript 78 | function* loginFlow() { 79 | while (true) { 80 | // ... 81 | try { 82 | const token = yield call(authorize, user, password) 83 | // ... 84 | } 85 | // ... 86 | } 87 | } 88 | ``` 89 | 90 | The user clicks on the `Logout` button causing a `LOGOUT` action to be dispatched. 91 | 92 | The following example illustrates the hypothetical sequence of the events: 93 | 94 | ``` 95 | UI loginFlow 96 | -------------------------------------------------------- 97 | LOGIN_REQUEST...................call authorize.......... waiting to resolve 98 | ........................................................ 99 | ........................................................ 100 | LOGOUT.................................................. missed! 101 | ........................................................ 102 | ................................authorize returned...... dispatch a `LOGIN_SUCCESS`!! 103 | ........................................................ 104 | ``` 105 | 106 | When `loginFlow` is blocked on the `authorize` call, an eventual `LOGOUT` occurring in between the call and the response will be missed, because `loginFlow` hasn't yet performed the `yield take('LOGOUT')`. 107 | 108 | The problem with the above code is that `call` is a blocking Effect. i.e. the Generator can't perform/handle anything else until the call terminates. But in our case we do not only want `loginFlow` to execute the authorization call, but also watch for an eventual `LOGOUT` action that may occur in the middle of this call. That's because `LOGOUT` is *concurrent* to the `authorize` call. 109 | 110 | So what's needed is some way to start `authorize` without blocking so `loginFlow` can continue and watch for an eventual/concurrent `LOGOUT` action. 111 | 112 | To express non-blocking calls, the library provides another Effect: [`fork`](https://redux-saga.js.org/docs/api/index.html#forkfn-args). When we fork a *task*, the task is started in the background and the caller can continue its flow without waiting for the forked task to terminate. 113 | 114 | So in order for `loginFlow` to not miss a concurrent `LOGOUT`, we must not `call` the `authorize` task, instead we have to `fork` it. 115 | 116 | ```javascript 117 | import { fork, call, take, put } from 'redux-saga/effects' 118 | 119 | function* loginFlow() { 120 | while (true) { 121 | ... 122 | try { 123 | // non-blocking call, what's the returned value here ? 124 | const ?? = yield fork(authorize, user, password) 125 | ... 126 | } 127 | ... 128 | } 129 | } 130 | ``` 131 | 132 | The issue now is since our `authorize` action is started in the background, we can't get the `token` result (because we'd have to wait for it). So we need to move the token storage operation into the `authorize` task. 133 | 134 | ```javascript 135 | import { fork, call, take, put } from 'redux-saga/effects' 136 | import Api from '...' 137 | 138 | function* authorize(user, password) { 139 | try { 140 | const token = yield call(Api.authorize, user, password) 141 | yield put({type: 'LOGIN_SUCCESS', token}) 142 | yield call(Api.storeItem, {token}) 143 | } catch(error) { 144 | yield put({type: 'LOGIN_ERROR', error}) 145 | } 146 | } 147 | 148 | function* loginFlow() { 149 | while (true) { 150 | const {user, password} = yield take('LOGIN_REQUEST') 151 | yield fork(authorize, user, password) 152 | yield take(['LOGOUT', 'LOGIN_ERROR']) 153 | yield call(Api.clearItem, 'token') 154 | } 155 | } 156 | ``` 157 | 158 | We're also doing `yield take(['LOGOUT', 'LOGIN_ERROR'])`. It means we are watching for 2 concurrent actions: 159 | 160 | - If the `authorize` task succeeds before the user logs out, it'll dispatch a `LOGIN_SUCCESS` action, then terminate. Our `loginFlow` saga will then wait only for a future `LOGOUT` action (because `LOGIN_ERROR` will never happen). 161 | 162 | - If the `authorize` fails before the user logs out, it will dispatch a `LOGIN_ERROR` action, then terminate. So `loginFlow` will take the `LOGIN_ERROR` before the `LOGOUT` then it will enter in a another `while` iteration and will wait for the next `LOGIN_REQUEST` action. 163 | 164 | - If the user logs out before the `authorize` terminate, then `loginFlow` will take a `LOGOUT` action and also wait for the next `LOGIN_REQUEST`. 165 | 166 | Note the call for `Api.clearItem` is supposed to be idempotent. It'll have no effect if no token was stored by the `authorize` call. `loginFlow` makes sure no token will be in the storage before waiting for the next login. 167 | 168 | But we're not yet done. If we take a `LOGOUT` in the middle of an API call, we have to **cancel** the `authorize` process, otherwise we'll have 2 concurrent tasks evolving in parallel: The `authorize` task will continue running and upon a successful (resp. failed) result, will dispatch a `LOGIN_SUCCESS` (resp. a `LOGIN_ERROR`) action leading to an inconsistent state. 169 | 170 | In order to cancel a forked task, we use a dedicated Effect [`cancel`](https://redux-saga.js.org/docs/api/index.html#canceltask) 171 | 172 | ```javascript 173 | import { take, put, call, fork, cancel } from 'redux-saga/effects' 174 | 175 | // ... 176 | 177 | function* loginFlow() { 178 | while (true) { 179 | const {user, password} = yield take('LOGIN_REQUEST') 180 | // fork return a Task object 181 | const task = yield fork(authorize, user, password) 182 | const action = yield take(['LOGOUT', 'LOGIN_ERROR']) 183 | if (action.type === 'LOGOUT') 184 | yield cancel(task) 185 | yield call(Api.clearItem, 'token') 186 | } 187 | } 188 | ``` 189 | 190 | `yield fork` results in a [Task Object](https://redux-saga.js.org/docs/api/index.html#task). We assign the returned object into a local constant `task`. Later if we take a `LOGOUT` action, we pass that task to the `cancel` Effect. If the task is still running, it'll be aborted. If the task has already completed then nothing will happen and the cancellation will result in a no-op. And finally, if the task completed with an error, then we do nothing, because we know the task already completed. 191 | 192 | We are *almost* done (concurrency is not that easy; you have to take it seriously). 193 | 194 | Suppose that when we receive a `LOGIN_REQUEST` action, our reducer sets some `isLoginPending` flag to true so it can display some message or spinner in the UI. If we get a `LOGOUT` in the middle of an API call and abort the task by simply *killing it* (i.e. the task is stopped right away), then we may end up again with an inconsistent state. We'll still have `isLoginPending` set to true and our reducer will be waiting for an outcome action (`LOGIN_SUCCESS` or `LOGIN_ERROR`). 195 | 196 | Fortunately, the `cancel` Effect won't brutally kill our `authorize` task, it'll instead give it a chance to perform its cleanup logic. The cancelled task can handle any cancellation logic (as well as any other type of completion) in its `finally` block. Since a finally block execute on any type of completion (normal return, error, or forced cancellation), there is an Effect `cancelled` which you can use if you want handle cancellation in a special way: 197 | 198 | ```javascript 199 | import { take, call, put, cancelled } from 'redux-saga/effects' 200 | import Api from '...' 201 | 202 | function* authorize(user, password) { 203 | try { 204 | const token = yield call(Api.authorize, user, password) 205 | yield put({type: 'LOGIN_SUCCESS', token}) 206 | yield call(Api.storeItem, {token}) 207 | return token 208 | } catch(error) { 209 | yield put({type: 'LOGIN_ERROR', error}) 210 | } finally { 211 | if (yield cancelled()) { 212 | // ... put special cancellation handling code here 213 | } 214 | } 215 | } 216 | ``` 217 | 218 | You may have noticed that we haven't done anything about clearing our `isLoginPending` state. For that, there are at least two possible solutions: 219 | 220 | - dispatch a dedicated action `RESET_LOGIN_PENDING` 221 | - more simply, make the reducer clear the `isLoginPending` on a `LOGOUT` action 222 | -------------------------------------------------------------------------------- /docs/advanced/README.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | 3 | In this section, we'll dig into more powerful Effects provided by the library. 4 | 5 | * [Pulling future actions](FutureActions.md) 6 | * [Non-blocking calls](NonBlockingCalls.md) 7 | * [Running tasks in parallel](RunningTasksInParallel.md) 8 | * [Starting a race between multiple Effects](RacingEffects.md) 9 | * [Sequencing Sagas using yield*](SequencingSagas.md) 10 | * [Composing Sagas](ComposingSagas.md) 11 | * [Task cancellation](TaskCancellation.md) 12 | * [redux-saga's fork model](ForkModel.md) 13 | * [Common Concurrency Patterns](Concurrency.md) 14 | * [Examples of Testing Sagas](Testing.md) 15 | * [Connecting Sagas to external Input/Output](UsingRunSaga.md) 16 | * [Using Channels](Channels.md) 17 | -------------------------------------------------------------------------------- /docs/advanced/RacingEffects.md: -------------------------------------------------------------------------------- 1 | ## Starting a race between multiple Effects 2 | 3 | Sometimes we start multiple tasks in parallel but we don't want to wait for all of them, we just need 4 | to get the *winner*: the first one that resolves (or rejects). The `race` Effect offers a way of 5 | triggering a race between multiple Effects. 6 | 7 | The following sample shows a task that triggers a remote fetch request, and constrains the response within a 8 | 1 second timeout. 9 | 10 | ```javascript 11 | import { race, take, put } from 'redux-saga/effects' 12 | import { delay } from 'redux-saga' 13 | 14 | function* fetchPostsWithTimeout() { 15 | const {posts, timeout} = yield race({ 16 | posts: call(fetchApi, '/posts'), 17 | timeout: call(delay, 1000) 18 | }) 19 | 20 | if (posts) 21 | put({type: 'POSTS_RECEIVED', posts}) 22 | else 23 | put({type: 'TIMEOUT_ERROR'}) 24 | } 25 | ``` 26 | 27 | Another useful feature of `race` is that it automatically cancels the loser Effects. For example, 28 | suppose we have 2 UI buttons: 29 | 30 | - The first starts a task in the background that runs in an endless loop `while (true)` 31 | (e.g. syncing some data with the server each x seconds). 32 | 33 | - Once the background task is started, we enable a second button which will cancel the task 34 | 35 | 36 | ```javascript 37 | import { race, take, put } from 'redux-saga/effects' 38 | 39 | function* backgroundTask() { 40 | while (true) { ... } 41 | } 42 | 43 | function* watchStartBackgroundTask() { 44 | while (true) { 45 | yield take('START_BACKGROUND_TASK') 46 | yield race({ 47 | task: call(backgroundTask), 48 | cancel: take('CANCEL_TASK') 49 | }) 50 | } 51 | } 52 | ``` 53 | 54 | In the case a `CANCEL_TASK` action is dispatched, the `race` Effect will automatically cancel 55 | `backgroundTask` by throwing a cancellation error inside it. 56 | -------------------------------------------------------------------------------- /docs/advanced/RunningTasksInParallel.md: -------------------------------------------------------------------------------- 1 | # Running Tasks In Parallel 2 | 3 | The `yield` statement is 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: 4 | 5 | ```javascript 6 | // wrong, effects will be executed in sequence 7 | const users = yield call(fetch, '/users'), 8 | repos = yield call(fetch, '/repos') 9 | ``` 10 | 11 | Because the 2nd effect will not get executed until the first call resolves. Instead we have to write: 12 | 13 | ```javascript 14 | import { call } from 'redux-saga/effects' 15 | 16 | // correct, effects will get executed in parallel 17 | const [users, repos] = yield [ 18 | call(fetch, '/users'), 19 | call(fetch, '/repos') 20 | ] 21 | ``` 22 | 23 | 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). 24 | -------------------------------------------------------------------------------- /docs/advanced/SequencingSagas.md: -------------------------------------------------------------------------------- 1 | # Sequencing Sagas via `yield*` 2 | 3 | You can use the builtin `yield*` operator to compose multiple Sagas in a sequential way. This allows you to sequence your *macro-tasks* in a simple procedural style. 4 | 5 | ```javascript 6 | function* playLevelOne() { ... } 7 | 8 | function* playLevelTwo() { ... } 9 | 10 | function* playLevelThree() { ... } 11 | 12 | function* game() { 13 | const score1 = yield* playLevelOne() 14 | yield put(showScore(score1)) 15 | 16 | const score2 = yield* playLevelTwo() 17 | yield put(showScore(score2)) 18 | 19 | const score3 = yield* playLevelThree() 20 | yield put(showScore(score3)) 21 | } 22 | ``` 23 | 24 | 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. 25 | -------------------------------------------------------------------------------- /docs/advanced/TaskCancellation.md: -------------------------------------------------------------------------------- 1 | # Task cancellation 2 | 3 | We saw already an example of cancellation in the [Non blocking calls](NonBlockingCalls.md) section. In this section we'll review cancellation in more detail. 4 | 5 | Once a task is forked, you can abort its execution using `yield cancel(task)`. 6 | 7 | To see how it works, let's consider a simple example: A background sync which can be started/stopped by some UI commands. Upon receiving a `START_BACKGROUND_SYNC` action, we fork a background task that will periodically sync some data from a remote server. 8 | 9 | The task will execute continually until a `STOP_BACKGROUND_SYNC` action is triggered. Then we cancel the background task and wait again for the next `START_BACKGROUND_SYNC` action. 10 | 11 | ```javascript 12 | import { take, put, call, fork, cancel, cancelled } from 'redux-saga/effects' 13 | import { delay } from 'redux-saga' 14 | import { someApi, actions } from 'somewhere' 15 | 16 | function* bgSync() { 17 | try { 18 | while (true) { 19 | yield put(actions.requestStart()) 20 | const result = yield call(someApi) 21 | yield put(actions.requestSuccess(result)) 22 | yield call(delay, 5000) 23 | } 24 | } finally { 25 | if (yield cancelled()) 26 | yield put(actions.requestFailure('Sync cancelled!')) 27 | } 28 | } 29 | 30 | function* main() { 31 | while ( yield take(START_BACKGROUND_SYNC) ) { 32 | // starts the task in the background 33 | const bgSyncTask = yield fork(bgSync) 34 | 35 | // wait for the user stop action 36 | yield take(STOP_BACKGROUND_SYNC) 37 | // user clicked stop. cancel the background task 38 | // this will cause the forked bgSync task to jump into its finally block 39 | yield cancel(bgSyncTask) 40 | } 41 | } 42 | ``` 43 | 44 | In the above example, cancellation of `bgSyncTask` will cause the Generator to jump to the finally block. Here you can use `yield cancelled()` to check if the Generator has been cancelled or not. 45 | 46 | Cancelling a running task will also cancel the current Effect where the task is blocked at the moment of cancellation. 47 | 48 | For example, suppose that at a certain point in an application's lifetime, we have this pending call chain: 49 | 50 | ```javascript 51 | function* main() { 52 | const task = yield fork(subtask) 53 | ... 54 | // later 55 | yield cancel(task) 56 | } 57 | 58 | function* subtask() { 59 | ... 60 | yield call(subtask2) // currently blocked on this call 61 | ... 62 | } 63 | 64 | function* subtask2() { 65 | ... 66 | yield call(someApi) // currently blocked on this call 67 | ... 68 | } 69 | ``` 70 | 71 | `yield cancel(task)` triggers a cancellation on `subtask`, which in turn triggers a cancellation on `subtask2`. 72 | 73 | So we saw that Cancellation propagates downward (in contrast returned values and uncaught errors propagates upward). You can see it as a *contract* between the caller (which invokes the async operation) and the callee (the invoked operation). The callee is responsible for performing the operation. If it has completed (either success or error) the outcome propagates up to its caller and eventually to the caller of the caller and so on. That is, callees are responsible for *completing the flow*. 74 | 75 | Now if the callee is still pending and the caller decides to cancel the operation, it triggers a kind of a signal that propagates down to the callee (and possibly to any deep operations called by the callee itself). All deeply pending operations will be cancelled. 76 | 77 | There is another direction where the cancellation propagates to as well: the joiners of a task (those blocked on a `yield join(task)`) will also be cancelled if the joined task is cancelled. Similarly, any potential callers of those joiners will be cancelled as well (because they are blocked on an operation that has been cancelled from outside). 78 | 79 | ## Testing generators with fork effect 80 | 81 | When `fork` is called it starts the task in the background and also returns task object like we have learned previously. When testing this we have to use utility function `createMockTask`. Object returned from this function should be passed to next `next` call after fork test. Mock task can then be passed to `cancel` for example. Here is test for `main` generator which is on top of this page. 82 | 83 | ```javascript 84 | import { createMockTask } from 'redux-saga/utils'; 85 | 86 | describe('main', () => { 87 | const generator = main(); 88 | 89 | it('waits for start action', () => { 90 | const expectedYield = take(START_BACKGROUND_SYNC); 91 | expect(generator.next().value).to.deep.equal(expectedYield); 92 | }); 93 | 94 | it('forks the service', () => { 95 | const expectedYield = fork(bgSync); 96 | expect(generator.next().value).to.deep.equal(expectedYield); 97 | }); 98 | 99 | it('waits for stop action and then cancels the service', () => { 100 | const mockTask = createMockTask(); 101 | 102 | const expectedTakeYield = take(STOP_BACKGROUND_SYNC); 103 | expect(generator.next(mockTask).value).to.deep.equal(expectedTakeYield); 104 | 105 | const expectedCancelYield = cancel(mockTask); 106 | expect(generator.next().value).to.deep.equal(expectedCancelYield); 107 | }); 108 | }); 109 | ``` 110 | 111 | You can also use mock task's functions `setRunning`, `setResult` and `setError` to set mock task's state. For example `mockTask.setRunning(false)`. 112 | 113 | ### Note 114 | 115 | It's important to remember that `yield cancel(task)` doesn't wait for the cancelled task to finish (i.e. to perform its finally block). The cancel effect behaves like fork. It returns as soon as the cancel was initiated. Once cancelled, a task should normally return as soon as it finishes its cleanup logic. 116 | 117 | ## Automatic cancellation 118 | 119 | Besides manual cancellation there are cases where cancellation is triggered automatically 120 | 121 | 1. In a `race` effect. All race competitors, except the winner, are automatically cancelled. 122 | 123 | 2. In a parallel effect (`yield [...]`). The parallel effect is rejected as soon as one of the sub-effects is rejected (as implied by `Promise.all`). In this case, all the other sub-effects are automatically cancelled. 124 | -------------------------------------------------------------------------------- /docs/advanced/Testing.md: -------------------------------------------------------------------------------- 1 | # Testing Sagas 2 | 3 | **Effects return plain javascript objects** 4 | 5 | Those objects describe the effect and redux-saga is in charge to execute them. 6 | 7 | This makes testing very easy because all you have to do is compare that the object yielded by the saga describe the effect you want. 8 | 9 | ## Basic Example 10 | 11 | ```javascript 12 | console.log(put({ type: MY_CRAZY_ACTION })); 13 | 14 | /* 15 | { 16 | @@redux-saga/IO': true, 17 | PUT: { 18 | channel: null, 19 | action: { 20 | type: 'MY_CRAZY_ACTION' 21 | } 22 | } 23 | } 24 | */ 25 | ``` 26 | 27 | Testing a saga that wait for a user action and dispatch 28 | 29 | ```javascript 30 | const CHOOSE_COLOR = 'CHOOSE_COLOR'; 31 | const CHANGE_UI = 'CHANGE_UI'; 32 | 33 | const chooseColor = (color) => ({ 34 | type: CHOOSE_COLOR, 35 | payload: { 36 | color, 37 | }, 38 | }); 39 | 40 | const changeUI = (color) => ({ 41 | type: CHANGE_UI, 42 | payload: { 43 | color, 44 | }, 45 | }); 46 | 47 | 48 | function* changeColorSaga() { 49 | const action = yield take(CHOOSE_COLOR); 50 | yield put(changeUI(action.payload.color)); 51 | } 52 | 53 | test('change color saga', assert => { 54 | const gen = changeColorSaga(); 55 | 56 | assert.deepEqual( 57 | gen.next().value, 58 | take(CHOOSE_COLOR), 59 | 'it should wait for a user to choose a color' 60 | ); 61 | 62 | const color = 'red'; 63 | assert.deepEqual( 64 | gen.next(chooseColor(color)).value, 65 | put(changeUI(color)), 66 | 'it should dispatch an action to change the ui' 67 | ); 68 | 69 | assert.deepEqual( 70 | gen.next().done, 71 | true, 72 | 'it should be done' 73 | ); 74 | 75 | assert.end(); 76 | }); 77 | ``` 78 | 79 | Another great benefit is that your tests are also your doc! They describe everything that should happen. 80 | 81 | ## Branching Saga 82 | 83 | Sometimes your saga will have different outcomes. To test the different branches without repeating all the steps that lead to it you can use the utility function **cloneableGenerator** 84 | ```javascript 85 | const CHOOSE_NUMBER = 'CHOOSE_NUMBER'; 86 | const CHANGE_UI = 'CHANGE_UI'; 87 | const DO_STUFF = 'DO_STUFF'; 88 | 89 | const chooseNumber = (number) => ({ 90 | type: CHOOSE_NUMBER, 91 | payload: { 92 | number, 93 | }, 94 | }); 95 | 96 | const changeUI = (color) => ({ 97 | type: CHANGE_UI, 98 | payload: { 99 | color, 100 | }, 101 | }); 102 | 103 | const doStuff = () => ({ 104 | type: DO_STUFF, 105 | }); 106 | 107 | 108 | function* doStuffThenChangeColor() { 109 | yield put(doStuff()); 110 | yield put(doStuff()); 111 | const action = yield take(CHOOSE_NUMBER); 112 | if (action.payload.number % 2 === 0) { 113 | yield put(changeUI('red')); 114 | } else { 115 | yield put(changeUI('blue')); 116 | } 117 | } 118 | 119 | import { put, take } from 'redux-saga/effects'; 120 | import { cloneableGenerator } from 'redux-saga/utils'; 121 | 122 | test('doStuffThenChangeColor', assert => { 123 | const data = {}; 124 | data.gen = cloneableGenerator(doStuffThenChangeColor)(); 125 | 126 | assert.deepEqual( 127 | data.gen.next().value, 128 | put(doStuff()), 129 | 'it should do stuff' 130 | ); 131 | 132 | assert.deepEqual( 133 | data.gen.next().value, 134 | put(doStuff()), 135 | 'it should do stuff' 136 | ); 137 | 138 | assert.deepEqual( 139 | data.gen.next().value, 140 | take(CHOOSE_NUMBER), 141 | 'should wait for the user to give a number' 142 | ); 143 | 144 | assert.test('user choose an even number', a => { 145 | // cloning the generator before sending data 146 | data.clone = data.gen.clone(); 147 | a.deepEqual( 148 | data.gen.next(chooseNumber(2)).value, 149 | put(changeUI('red')), 150 | 'should change the color to red' 151 | ); 152 | 153 | a.equal( 154 | data.gen.next().done, 155 | true, 156 | 'it should be done' 157 | ); 158 | 159 | a.end(); 160 | }); 161 | 162 | assert.test('user choose an odd number', a => { 163 | a.deepEqual( 164 | data.clone.next(chooseNumber(3)).value, 165 | put(changeUI('blue')), 166 | 'should change the color to blue' 167 | ); 168 | 169 | a.equal( 170 | data.clone.next().done, 171 | true, 172 | 'it should be done' 173 | ); 174 | 175 | a.end(); 176 | }); 177 | }); 178 | ``` 179 | 180 | See also: [Task cancellation](TaskCancellation.md) for testing fork effects 181 | 182 | See also: Repository Examples: 183 | 184 | https://github.com/redux-saga/redux-saga/blob/master/examples/counter/test/sagas.js 185 | 186 | https://github.com/redux-saga/redux-saga/blob/master/examples/shopping-cart/test/sagas.js 187 | -------------------------------------------------------------------------------- /docs/advanced/UsingRunSaga.md: -------------------------------------------------------------------------------- 1 | # Connecting Sagas to external Input/Output 2 | 3 | We saw that `take` Effects are resolved by waiting for actions to be dispatched to the Store. And that `put` Effects are resolved by dispatching the actions provided as argument. 4 | 5 | When a Saga is started (either at startup or later dynamically), the middleware automatically connects its `take`/`put` to the store. The 2 Effects can be seen as a sort of Input/Output to the Saga. 6 | 7 | `redux-saga` provides a way to run a Saga outside of the Redux middleware environment and connect it to a custom Input/Output. 8 | 9 | ```javascript 10 | import { runSaga } from 'redux-saga' 11 | 12 | function* saga() { ... } 13 | 14 | const myIO = { 15 | subscribe: ..., // this will be used to resolve take Effects 16 | dispatch: ..., // this will be used to resolve put Effects 17 | getState: ..., // this will be used to resolve select Effects 18 | } 19 | 20 | runSaga( 21 | saga(), 22 | myIO 23 | ) 24 | ``` 25 | 26 | For more info, see the [API docs](https://redux-saga.js.org/docs/api/index.html#runsagaiterator-options). 27 | -------------------------------------------------------------------------------- /docs/basics/DeclarativeEffects.md: -------------------------------------------------------------------------------- 1 | # Declarative Effects 2 | 3 | In `redux-saga`, Sagas are implemented using Generator functions. To express the Saga logic we yield plain JavaScript Objects from the Generator. We call those Objects *Effects*. An Effect is simply an object which contains some information to be interpreted by the middleware. You can view Effects like instructions to the middleware to perform some operation (invoke some asynchronous function, dispatch an action to the store). 4 | 5 | To create Effects, you use the functions provided by the library in the `redux-saga/effects` package. 6 | 7 | In this section and the following, we will introduce some basic Effects. And see how the concept allows the Sagas to be easily tested. 8 | 9 | Sagas can yield Effects in multiple forms. The simplest way is to yield a Promise. 10 | 11 | For example suppose we have a Saga that watches a `PRODUCTS_REQUESTED` action. On each matching action, it starts a task to fetch a list of products from a server. 12 | 13 | ```javascript 14 | import { takeEvery } from 'redux-saga/effects' 15 | import Api from './path/to/api' 16 | 17 | function* watchFetchProducts() { 18 | yield takeEvery('PRODUCTS_REQUESTED', fetchProducts) 19 | } 20 | 21 | function* fetchProducts() { 22 | const products = yield Api.fetch('/products') 23 | console.log(products) 24 | } 25 | ``` 26 | 27 | In the example above, we are invoking `Api.fetch` directly from inside the Generator (In Generator functions, any expression at the right of `yield` is evaluated then the result is yielded to the caller). 28 | 29 | `Api.fetch('/products')` triggers an AJAX request and returns a Promise that will resolve with the resolved response, the AJAX request will be executed immediately. Simple and idiomatic, but... 30 | 31 | Suppose we want to test generator above: 32 | 33 | ```javascript 34 | const iterator = fetchProducts() 35 | assert.deepEqual(iterator.next().value, ??) // what do we expect ? 36 | ``` 37 | 38 | We want to check the result of the first value yielded by the generator. In our case it's the result of running `Api.fetch('/products')` which is a Promise . Executing the real service during tests is neither a viable nor practical approach, so we have to *mock* the `Api.fetch` function, i.e. we'll have to replace the real function with a fake one which doesn't actually run the AJAX request but only checks that we've called `Api.fetch` with the right arguments (`'/products'` in our case). 39 | 40 | Mocks make testing more difficult and less reliable. On the other hand, functions that simply return values are easier to test, since we can use a simple `equal()` to check the result. This is the way to write the most reliable tests. 41 | 42 | Not convinced? I encourage you to read [Eric Elliott's article](https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.4ttnnzpgc): 43 | 44 | > (...)`equal()`, by nature answers the two most important questions every unit test must answer, 45 | but most don’t: 46 | - What is the actual output? 47 | - What is the expected output? 48 | > 49 | > If you finish a test without answering those two questions, you don’t have a real unit test. You have a sloppy, half-baked test. 50 | 51 | What we actually need is just to make sure the `fetchProducts` task yields a call with the right function and the right arguments. 52 | 53 | Instead of invoking the asynchronous function directly from inside the Generator, **we can yield only a description of the function invocation**. i.e. We'll simply yield an object which looks like 54 | 55 | ```javascript 56 | // Effect -> call the function Api.fetch with `./products` as argument 57 | { 58 | CALL: { 59 | fn: Api.fetch, 60 | args: ['./products'] 61 | } 62 | } 63 | ``` 64 | 65 | Put another way, the Generator will yield plain Objects containing *instructions*, and the `redux-saga` middleware will take care of executing those instructions and giving back the result of their execution to the Generator. This way, when testing the Generator, all we need to do is to check that it yields the expected instruction by doing a simple `deepEqual` on the yielded Object. 66 | 67 | For this reason, the library provides a different way to perform asynchronous calls. 68 | 69 | ```javascript 70 | import { call } from 'redux-saga/effects' 71 | 72 | function* fetchProducts() { 73 | const products = yield call(Api.fetch, '/products') 74 | // ... 75 | } 76 | ``` 77 | 78 | We're using now the `call(fn, ...args)` function. **The difference from the preceding 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, `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. 79 | 80 | This allows us to easily test the Generator outside the Redux environment. Because `call` is just a function which returns a plain Object. 81 | 82 | ```javascript 83 | import { call } from 'redux-saga/effects' 84 | import Api from '...' 85 | 86 | const iterator = fetchProducts() 87 | 88 | // expects a call instruction 89 | assert.deepEqual( 90 | iterator.next().value, 91 | call(Api.fetch, '/products'), 92 | "fetchProducts should yield an Effect call(Api.fetch, './products')" 93 | ) 94 | ``` 95 | 96 | Now we don't need to mock anything, and a simple equality test will suffice. 97 | 98 | The advantage of those *declarative calls* is that we can test all the logic inside a Saga by simply iterating over the Generator and doing a `deepEqual` test on the values yielded successively. This is a real benefit, as your complex asynchronous operations are no longer black boxes, and you can test in detail their operational logic no matter how complex it is. 99 | 100 | `call` also supports invoking object methods, you can provide a `this` context to the invoked functions using the following form: 101 | 102 | ```javascript 103 | yield call([obj, obj.method], arg1, arg2, ...) // as if we did obj.method(arg1, arg2 ...) 104 | ``` 105 | 106 | `apply` is an alias for the method invocation form 107 | 108 | ```javascript 109 | yield apply(obj, obj.method, [arg1, arg2, ...]) 110 | ``` 111 | 112 | `call` and `apply` are 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` is of the form `(error, result) => ()`). `cps` stands for Continuation Passing Style. 113 | 114 | For example: 115 | 116 | ```javascript 117 | import { cps } from 'redux-saga/effects' 118 | 119 | const content = yield cps(readFile, '/path/to/file') 120 | ``` 121 | 122 | And of course you can test it just like you test `call`: 123 | 124 | ```javascript 125 | import { cps } from 'redux-saga/effects' 126 | 127 | const iterator = fetchSaga() 128 | assert.deepEqual(iterator.next().value, cps(readFile, '/path/to/file') ) 129 | ``` 130 | 131 | `cps` also supports the same method invocation form as `call`. 132 | -------------------------------------------------------------------------------- /docs/basics/DispatchingActions.md: -------------------------------------------------------------------------------- 1 | # Dispatching actions to the store 2 | 3 | Taking the previous example further, let's say that after each save, we want to dispatch some action 4 | to notify the Store that the fetch has succeeded (we'll omit the failure case for the moment). 5 | 6 | We could pass the Store's `dispatch` function to the Generator. Then the 7 | Generator could invoke it after receiving the fetch response: 8 | 9 | ```javascript 10 | // ... 11 | 12 | function* fetchProducts(dispatch) { 13 | const products = yield call(Api.fetch, '/products') 14 | dispatch({ type: 'PRODUCTS_RECEIVED', products }) 15 | } 16 | ``` 17 | 18 | However, this solution has the same drawbacks as invoking functions directly from inside the Generator (as discussed in the previous section). If we want to test that `fetchProducts` performs 19 | the dispatch after receiving the AJAX response, we'll need again to mock the `dispatch` 20 | function. 21 | 22 | Instead, we need the same declarative solution. Just create an Object to instruct the 23 | middleware that we need to dispatch some action, and let the middleware perform the real 24 | dispatch. This way we can test the Generator's dispatch in the same way: by just inspecting 25 | the yielded Effect and making sure it contains the correct instructions. 26 | 27 | The library provides, for this purpose, another function `put` which creates the dispatch 28 | Effect. 29 | 30 | ```javascript 31 | import { call, put } from 'redux-saga/effects' 32 | // ... 33 | 34 | function* fetchProducts() { 35 | const products = yield call(Api.fetch, '/products') 36 | // create and yield a dispatch Effect 37 | yield put({ type: 'PRODUCTS_RECEIVED', products }) 38 | } 39 | ``` 40 | 41 | Now, we can test the Generator easily as in the previous section 42 | 43 | ```javascript 44 | import { call, put } from 'redux-saga/effects' 45 | import Api from '...' 46 | 47 | const iterator = fetchProducts() 48 | 49 | // expects a call instruction 50 | assert.deepEqual( 51 | iterator.next().value, 52 | call(Api.fetch, '/products'), 53 | "fetchProducts should yield an Effect call(Api.fetch, './products')" 54 | ) 55 | 56 | // create a fake response 57 | const products = {} 58 | 59 | // expects a dispatch instruction 60 | assert.deepEqual( 61 | iterator.next(products).value, 62 | put({ type: 'PRODUCTS_RECEIVED', products }), 63 | "fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })" 64 | ) 65 | ``` 66 | 67 | Note how we pass the fake response to the Generator via its `next` method. Outside the 68 | middleware environment, we have total control over the Generator, we can simulate a 69 | real environment by simply mocking results and resuming the Generator with them. Mocking 70 | data is a lot simpler than mocking functions and spying calls. 71 | -------------------------------------------------------------------------------- /docs/basics/Effect.md: -------------------------------------------------------------------------------- 1 | # A common abstraction: Effect 2 | 3 | To generalize, triggering Side Effects from inside a Saga is always done by yielding some declarative Effect. (You can also yield Promise directly, but this will make testing difficult as we saw in the first section.) 4 | 5 | What a Saga does is actually compose all those Effects together to implement the desired control flow. The simplest example is to sequence yielded Effects by just putting the yields one after another. You can also use the familiar control flow operators (`if`, `while`, `for`) to implement more sophisticated control flows. 6 | 7 | We saw that using Effects like `call` and `put`, combined with high-level APIs like `takeEvery` allows us to achieve the same things as `redux-thunk`, but with the added benefit of easy testability. 8 | 9 | But `redux-saga` provides another advantage over `redux-thunk`. In the Advanced section you'll encounter some more powerful Effects that let you express complex control flows while still allowing the same testability benefit. 10 | -------------------------------------------------------------------------------- /docs/basics/ErrorHandling.md: -------------------------------------------------------------------------------- 1 | # Error handling 2 | 3 | In this section we'll see how to handle the failure case from the previous example. Let's suppose that our API function `Api.fetch` returns a Promise which gets rejected when the remote fetch fails for some reason. 4 | 5 | We want to handle those errors inside our Saga by dispatching a `PRODUCTS_REQUEST_FAILED` action to the Store. 6 | 7 | We can catch errors inside the Saga using the familiar `try/catch` syntax. 8 | 9 | ```javascript 10 | import Api from './path/to/api' 11 | import { call, put } from 'redux-saga/effects' 12 | 13 | // ... 14 | 15 | function* fetchProducts() { 16 | try { 17 | const products = yield call(Api.fetch, '/products') 18 | yield put({ type: 'PRODUCTS_RECEIVED', products }) 19 | } 20 | catch(error) { 21 | yield put({ type: 'PRODUCTS_REQUEST_FAILED', error }) 22 | } 23 | } 24 | ``` 25 | 26 | In order to test the failure case, we'll use the `throw` method of the Generator 27 | 28 | ```javascript 29 | import { call, put } from 'redux-saga/effects' 30 | import Api from '...' 31 | 32 | const iterator = fetchProducts() 33 | 34 | // expects a call instruction 35 | assert.deepEqual( 36 | iterator.next().value, 37 | call(Api.fetch, '/products'), 38 | "fetchProducts should yield an Effect call(Api.fetch, './products')" 39 | ) 40 | 41 | // create a fake error 42 | const error = {} 43 | 44 | // expects a dispatch instruction 45 | assert.deepEqual( 46 | iterator.throw(error).value, 47 | put({ type: 'PRODUCTS_REQUEST_FAILED', error }), 48 | "fetchProducts should yield an Effect put({ type: 'PRODUCTS_REQUEST_FAILED', error })" 49 | ) 50 | ``` 51 | 52 | In this case, we're passing the `throw` method a fake error. This will cause the Generator to break the current flow and execute the catch block. 53 | 54 | Of course, you're not forced to handle your API errors inside `try`/`catch` blocks. You can also make your API service return a normal value with some error flag on it. For example, you can catch Promise rejections and map them to an object with an error field. 55 | 56 | ```javascript 57 | import Api from './path/to/api' 58 | import { call, put } from 'redux-saga/effects' 59 | 60 | function fetchProductsApi() { 61 | return Api.fetch('/products') 62 | .then(response => ({ response })) 63 | .catch(error => ({ error })) 64 | } 65 | 66 | function* fetchProducts() { 67 | const { response, error } = yield call(fetchProductsApi) 68 | if (response) 69 | yield put({ type: 'PRODUCTS_RECEIVED', products: response }) 70 | else 71 | yield put({ type: 'PRODUCTS_REQUEST_FAILED', error }) 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/basics/README.md: -------------------------------------------------------------------------------- 1 | # Basic Concepts 2 | 3 | * [Using Saga Helpers](UsingSagaHelpers.md) 4 | * [Declarative Effects](DeclarativeEffects.md) 5 | * [Dispatching Actions](DispatchingActions.md) 6 | * [Error Handling](ErrorHandling.md) 7 | * [A Common Abstraction: Effect](Effect.md) 8 | -------------------------------------------------------------------------------- /docs/basics/UsingSagaHelpers.md: -------------------------------------------------------------------------------- 1 | # Using Saga Helpers 2 | 3 | `redux-saga` provides some helper effects wrapping internal functions to spawn tasks when some specific actions are dispatched to the Store. 4 | 5 | The helper functions are built on top of the lower level API. In the advanced section, we'll see how those functions can be implemented. 6 | 7 | The first function, `takeEvery` is the most familiar and provides a behavior similar to `redux-thunk`. 8 | 9 | Let's illustrate with the common AJAX example. On each click on a Fetch button we dispatch a `FETCH_REQUESTED` action. We want to handle this action by launching a task that will fetch some data from the server. 10 | 11 | First we create the task that will perform the asynchronous action: 12 | 13 | ```javascript 14 | import { call, put } from 'redux-saga/effects' 15 | 16 | export function* fetchData(action) { 17 | try { 18 | const data = yield call(Api.fetchUser, action.payload.url) 19 | yield put({type: "FETCH_SUCCEEDED", data}) 20 | } catch (error) { 21 | yield put({type: "FETCH_FAILED", error}) 22 | } 23 | } 24 | ``` 25 | 26 | To launch the above task on each `FETCH_REQUESTED` action: 27 | 28 | ```javascript 29 | import { takeEvery } from 'redux-saga/effects' 30 | 31 | function* watchFetchData() { 32 | yield takeEvery('FETCH_REQUESTED', fetchData) 33 | } 34 | ``` 35 | 36 | In the above example, `takeEvery` allows multiple `fetchData` instances to be started concurrently. At a given moment, we can start a new `fetchData` task while there are still one or more previous `fetchData` tasks which have not yet terminated. 37 | 38 | If we want to only get the response of the latest request fired (e.g. to always display the latest version of data) we can use the `takeLatest` helper: 39 | 40 | ```javascript 41 | import { takeLatest } from 'redux-saga/effects' 42 | 43 | function* watchFetchData() { 44 | yield takeLatest('FETCH_REQUESTED', fetchData) 45 | } 46 | ``` 47 | 48 | Unlike `takeEvery`, `takeLatest` allows only one `fetchData` task to run at any moment. And it will be the latest started task. If a previous task is still running when another `fetchData` task is started, the previous task will be automatically cancelled. 49 | 50 | If you have multiple Sagas watching for different actions, you can create multiple watchers with those built-in helpers which will behave like there was `fork` used to spawn them (we'll talk about `fork` later. For now consider it to be an Effect that allows us to start multiple sagas in the background) 51 | 52 | For example: 53 | 54 | ```javascript 55 | import { takeEvery } from 'redux-saga' 56 | 57 | // FETCH_USERS 58 | function* fetchUsers(action) { ... } 59 | 60 | // CREATE_USER 61 | function* createUser(action) { ... } 62 | 63 | // use them in parallel 64 | export default function* rootSaga() { 65 | yield takeEvery('FETCH_USERS', fetchUsers) 66 | yield takeEvery('CREATE_USER', createUser) 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/introduction/BeginnerTutorial.md: -------------------------------------------------------------------------------- 1 | # Beginner Tutorial 2 | 3 | ## Objectives of this tutorial 4 | 5 | This tutorial attempts to introduce redux-saga in a (hopefully) accessible way. 6 | 7 | For our getting started tutorial, we are going to use the trivial Counter demo from the Redux repo. The application is quite simple but is a good fit to illustrate the basic concepts of redux-saga without being lost in excessive details. 8 | 9 | ### The initial setup 10 | 11 | Before we start, clone the [tutorial repository](https://github.com/redux-saga/redux-saga-beginner-tutorial). 12 | 13 | > The final code of this tutorial is located in the `sagas` branch. 14 | 15 | Then in the command line, run: 16 | 17 | ```sh 18 | $ cd redux-saga-beginner-tutorial 19 | $ npm install 20 | ``` 21 | 22 | To start the application, run: 23 | 24 | ```sh 25 | $ npm start 26 | ``` 27 | 28 | We are starting with the simplest use case: 2 buttons to `Increment` and `Decrement` a counter. Later, we will introduce asynchronous calls. 29 | 30 | If things go well, you should see 2 buttons `Increment` and `Decrement` along with a message below showing `Counter: 0`. 31 | 32 | > In case you encountered an issue with running the application. Feel free to create an issue on the [tutorial repo](https://github.com/redux-saga/redux-saga-beginner-tutorial/issues). 33 | 34 | ## Hello Sagas! 35 | 36 | We are going to create our first Saga. Following the tradition, we will write our 'Hello, world' version for Sagas. 37 | 38 | Create a file `sagas.js` then add the following snippet: 39 | 40 | ```javascript 41 | export function* helloSaga() { 42 | console.log('Hello Sagas!') 43 | } 44 | ``` 45 | 46 | So nothing scary, just a normal function (except for the `*`). All it does is print a greeting message into the console. 47 | 48 | In order to run our Saga, we need to: 49 | 50 | - create a Saga middleware with a list of Sagas to run (so far we have only one `helloSaga`) 51 | - connect the Saga middleware to the Redux store 52 | 53 | We will make the changes to `main.js`: 54 | 55 | ```javascript 56 | // ... 57 | import { createStore, applyMiddleware } from 'redux' 58 | import createSagaMiddleware from 'redux-saga' 59 | 60 | // ... 61 | import { helloSaga } from './sagas' 62 | 63 | const sagaMiddleware = createSagaMiddleware() 64 | const store = createStore( 65 | reducer, 66 | applyMiddleware(sagaMiddleware) 67 | ) 68 | sagaMiddleware.run(helloSaga) 69 | 70 | const action = type => store.dispatch({type}) 71 | 72 | // rest unchanged 73 | ``` 74 | 75 | First we import our Saga from the `./sagas` module. Then we create a middleware using the factory function `createSagaMiddleware` exported by the `redux-saga` library. 76 | 77 | Before running our `helloSaga`, we must connect our middleware to the Store using `applyMiddleware`. Then we can use the `sagaMiddleware.run(helloSaga)` to start our Saga. 78 | 79 | So far, our Saga does nothing special. It just logs a message then exits. 80 | 81 | ## Making Asynchronous calls 82 | 83 | Now let's add something closer to the original Counter demo. To illustrate asynchronous calls, we will add another button to increment the counter 1 second after the click. 84 | 85 | First thing's first, we'll provide an additional callback `onIncrementAsync` to the UI component. 86 | 87 | ```javascript 88 | const Counter = ({ value, onIncrement, onDecrement, onIncrementAsync }) => 89 |
90 | {' '} 91 | 94 |
95 |
96 | Clicked: {value} times 97 |
98 |
99 | ``` 100 | 101 | Next we should connect the `onIncrementAsync` of the Component to a Store action. 102 | 103 | We will modify the `main.js` module as follows 104 | 105 | ```javascript 106 | function render() { 107 | ReactDOM.render( 108 | action('INCREMENT')} 111 | onDecrement={() => action('DECREMENT')} 112 | onIncrementAsync={() => action('INCREMENT_ASYNC')} />, 113 | document.getElementById('root') 114 | ) 115 | } 116 | ``` 117 | 118 | Note that unlike in redux-thunk, our component dispatches a plain object action. 119 | 120 | Now we will introduce another Saga to perform the asynchronous call. Our use case is as follows: 121 | 122 | > On each `INCREMENT_ASYNC` action, we want to start a task that will do the following 123 | 124 | > - Wait 1 second then increment the counter 125 | 126 | Add the following code to the `sagas.js` module: 127 | 128 | ```javascript 129 | import { delay } from 'redux-saga' 130 | import { put, takeEvery } from 'redux-saga/effects' 131 | 132 | // Our worker Saga: will perform the async increment task 133 | export function* incrementAsync() { 134 | yield delay(1000) 135 | yield put({ type: 'INCREMENT' }) 136 | } 137 | 138 | // Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC 139 | export function* watchIncrementAsync() { 140 | yield takeEvery('INCREMENT_ASYNC', incrementAsync) 141 | } 142 | ``` 143 | 144 | Time for some explanations. 145 | 146 | We import `delay`, a utility function that returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that will resolve after a specified number of milliseconds. We'll use this function to *block* the Generator. 147 | 148 | Sagas are implemented as [Generator functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) that *yield* objects to the redux-saga middleware. The yielded objects are a kind of instruction to be interpreted by the middleware. When a Promise is yielded to the middleware, the middleware will suspend the Saga until the Promise completes. In the above example, the `incrementAsync` Saga is suspended until the Promise returned by `delay` resolves, which will happen after 1 second. 149 | 150 | Once the Promise is resolved, the middleware will resume the Saga, executing code until the next yield. In this example, the next statement is another yielded object: the result of calling `put({type: 'INCREMENT'})`, which instructs the middleware to dispatch an `INCREMENT` action. 151 | 152 | `put` is one example of what we call an *Effect*. Effects are simple JavaScript objects which contain instructions to be fulfilled by the middleware. When a middleware retrieves an Effect yielded by a Saga, the Saga is paused until the Effect is fulfilled. 153 | 154 | So to summarize, the `incrementAsync` Saga sleeps for 1 second via the call to `delay(1000)`, then dispatches an `INCREMENT` action. 155 | 156 | Next, we created another Saga `watchIncrementAsync`. We use `takeEvery`, a helper function provided by `redux-saga`, to listen for dispatched `INCREMENT_ASYNC` actions and run `incrementAsync` each time. 157 | 158 | Now we have 2 Sagas, and we need to start them both at once. To do that, we'll add a `rootSaga` that is responsible for starting our other Sagas. In the same file `sagas.js`, add the following code: 159 | 160 | ```javascript 161 | // single entry point to start all Sagas at once 162 | export default function* rootSaga() { 163 | yield [ 164 | incrementAsync(), 165 | watchIncrementAsync() 166 | ] 167 | } 168 | ``` 169 | 170 | This Saga yields an array with the results of calling our two sagas, `helloSaga` and `watchIncrementAsync`. This means the two resulting Generators will be started in parallel. Now we only have to invoke `sagaMiddleware.run` on the root Saga in `main.js`. 171 | 172 | ```javascript 173 | // ... 174 | import rootSaga from './sagas' 175 | 176 | const sagaMiddleware = createSagaMiddleware() 177 | const store = ... 178 | sagaMiddleware.run(rootSaga) 179 | 180 | // ... 181 | ``` 182 | 183 | ## Making our code testable 184 | 185 | We want to test our `incrementAsync` Saga to make sure it performs the desired task. 186 | 187 | Create another file `sagas.spec.js`: 188 | 189 | ```javascript 190 | import test from 'tape'; 191 | 192 | import { incrementAsync } from './sagas' 193 | 194 | test('incrementAsync Saga test', (assert) => { 195 | const gen = incrementAsync() 196 | 197 | // now what ? 198 | }); 199 | ``` 200 | 201 | `incrementAsync` is a generator function. When run, it returns an iterator object, and the iterator's `next` method returns an object with the following shape 202 | 203 | ```javascript 204 | gen.next() // => { done: boolean, value: any } 205 | ``` 206 | 207 | The `value` field contains the yielded expression, i.e. the result of the expression after 208 | the `yield`. The `done` field indicates if the generator has terminated or if there are still 209 | more 'yield' expressions. 210 | 211 | In the case of `incrementAsync`, the generator yields 2 values consecutively: 212 | 213 | 1. `yield delay(1000)` 214 | 2. `yield put({type: 'INCREMENT'})` 215 | 216 | So if we invoke the next method of the generator 3 times consecutively we get the following 217 | results: 218 | 219 | ```javascript 220 | gen.next() // => { done: false, value: } 221 | gen.next() // => { done: false, value: } 222 | gen.next() // => { done: true, value: undefined } 223 | ``` 224 | 225 | The first 2 invocations return the results of the yield expressions. On the 3rd invocation 226 | since there is no more yield the `done` field is set to true. And since the `incrementAsync` 227 | Generator doesn't return anything (no `return` statement), the `value` field is set to 228 | `undefined`. 229 | 230 | So now, in order to test the logic inside `incrementAsync`, we'll simply have to iterate 231 | over the returned Generator and check the values yielded by the generator. 232 | 233 | ```javascript 234 | import test from 'tape'; 235 | 236 | import { incrementAsync } from './sagas' 237 | 238 | test('incrementAsync Saga test', (assert) => { 239 | const gen = incrementAsync() 240 | 241 | assert.deepEqual( 242 | gen.next(), 243 | { done: false, value: ??? }, 244 | 'incrementAsync should return a Promise that will resolve after 1 second' 245 | ) 246 | }); 247 | ``` 248 | 249 | The issue is how do we test the return value of `delay`? We can't do a simple equality test 250 | on Promises. If `delay` returned a *normal* value, things would've been easier to test. 251 | 252 | Well, `redux-saga` provides a way to make the above statement possible. Instead of calling 253 | `delay(1000)` directly inside `incrementAsync`, we'll call it *indirectly*: 254 | 255 | ```javascript 256 | // ... 257 | import { delay } from 'redux-saga' 258 | import { put, call, takeEvery } from 'redux-saga/effects' 259 | 260 | export function* incrementAsync() { 261 | // use the call Effect 262 | yield call(delay, 1000) 263 | yield put({ type: 'INCREMENT' }) 264 | } 265 | ``` 266 | 267 | Instead of doing `yield delay(1000)`, we're now doing `yield call(delay, 1000)`. What's the difference? 268 | 269 | In the first case, the yield expression `delay(1000)` is evaluated before it gets passed to the caller of `next` (the caller could be the middleware when running our code. It could also be our test code which runs the Generator function and iterates over the returned Generator). So what the caller gets is a Promise, like in the test code above. 270 | 271 | In the second case, the yield expression `call(delay, 1000)` is what gets passed to the caller of `next`. `call` just like `put`, returns an Effect which instructs the middleware to call a given function with the given arguments. In fact, neither `put` nor `call` performs any dispatch or asynchronous call by themselves, they simply return plain JavaScript objects. 272 | 273 | ```javascript 274 | put({type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} } 275 | call(delay, 1000) // => { CALL: {fn: delay, args: [1000]}} 276 | ``` 277 | 278 | What happens is that the middleware examines the type of each yielded Effect then decides how to fulfill that Effect. If the Effect type is a `PUT` then it will dispatch an action to the Store. If the Effect is a `CALL` then it'll call the given function. 279 | 280 | This separation between Effect creation and Effect execution makes it possible to test our Generator in a surprisingly easy way: 281 | 282 | ```javascript 283 | import test from 'tape'; 284 | 285 | import { put, call } from 'redux-saga/effects' 286 | import { delay } from 'redux-saga' 287 | import { incrementAsync } from './sagas' 288 | 289 | test('incrementAsync Saga test', (assert) => { 290 | const gen = incrementAsync() 291 | 292 | assert.deepEqual( 293 | gen.next().value, 294 | call(delay, 1000), 295 | 'incrementAsync Saga must call delay(1000)' 296 | ) 297 | 298 | assert.deepEqual( 299 | gen.next().value, 300 | put({type: 'INCREMENT'}), 301 | 'incrementAsync Saga must dispatch an INCREMENT action' 302 | ) 303 | 304 | assert.deepEqual( 305 | gen.next(), 306 | { done: true, value: undefined }, 307 | 'incrementAsync Saga must be done' 308 | ) 309 | 310 | assert.end() 311 | }); 312 | ``` 313 | 314 | Since `put` and `call` return plain objects, we can reuse the same functions in our test code. And to test the logic of `incrementAsync`, we simply iterate over the generator and do `deepEqual` tests on its values. 315 | 316 | In order to run the above test, run: 317 | 318 | ```sh 319 | $ npm test 320 | ``` 321 | 322 | which should report the results on the console. 323 | -------------------------------------------------------------------------------- /docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | * [Beginner Tutorial](BeginnerTutorial.md) 4 | * [Background on the Saga concept](SagaBackground.md) 5 | -------------------------------------------------------------------------------- /docs/introduction/SagaBackground.md: -------------------------------------------------------------------------------- 1 | # Background on the Saga concept 2 | 3 | **WIP** 4 | 5 | For now, here are some useful links. 6 | 7 | ## External links 8 | 9 | - [Applying the Saga Pattern (Youtube video)](https://www.youtube.com/watch?v=xDuwrtwYHu8) By Caitie McCaffrey 10 | - [Original paper](http://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf) By Hector Garcia-Molina & Kenneth Salem 11 | - [A Saga on Sagas](https://msdn.microsoft.com/en-us/library/jj591569.aspx) from MSDN site 12 | -------------------------------------------------------------------------------- /docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | ## Throttling 4 | 5 | You can throttle a sequence of dispatched actions by using a handy built-in `throttle` helper. For example, suppose the UI fires an `INPUT_CHANGED` action while the user is typing in a text field. 6 | 7 | ```javascript 8 | import { throttle } from 'redux-saga/effects' 9 | 10 | function* handleInput(input) { 11 | // ... 12 | } 13 | 14 | function* watchInput() { 15 | yield throttle(500, 'INPUT_CHANGED', handleInput) 16 | } 17 | ``` 18 | 19 | By using this helper the `watchInput` won't start a new `handleInput` task for 500ms, but in the same time it will still be accepting the latest `INPUT_CHANGED` actions into its underlaying `buffer`, so it'll miss all `INPUT_CHANGED` actions happening in-between. This ensures that the Saga will take at most one `INPUT_CHANGED` action during each period of 500ms and still be able to process trailing action. 20 | 21 | ## Debouncing 22 | 23 | To debounce a sequence, put the built-in `delay` helper in the forked task: 24 | 25 | ```javascript 26 | 27 | import { delay } from 'redux-saga' 28 | 29 | function* handleInput(input) { 30 | // debounce by 500ms 31 | yield call(delay, 500) 32 | ... 33 | } 34 | 35 | function* watchInput() { 36 | let task 37 | while (true) { 38 | const { input } = yield take('INPUT_CHANGED') 39 | if (task) { 40 | yield cancel(task) 41 | } 42 | task = yield fork(handleInput, input) 43 | } 44 | } 45 | ``` 46 | 47 | The `delay` function implements a simple debounce using a Promise. 48 | ``` 49 | const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)) 50 | ``` 51 | 52 | In the above example `handleInput` waits for 500ms before performing its logic. If the user types something during this period we'll get more `INPUT_CHANGED` actions. Since `handleInput` will still be blocked in the `delay` call, it'll be cancelled by `watchInput` before it can start performing its logic. 53 | 54 | Example above could be rewritten with redux-saga `takeLatest` helper: 55 | 56 | ```javascript 57 | 58 | import { delay } from 'redux-saga' 59 | 60 | function* handleInput({ input }) { 61 | // debounce by 500ms 62 | yield call(delay, 500) 63 | ... 64 | } 65 | 66 | function* watchInput() { 67 | // will cancel current running handleInput task 68 | yield takeLatest('INPUT_CHANGED', handleInput); 69 | } 70 | ``` 71 | 72 | ## Retrying XHR calls 73 | 74 | To retry a XHR call for a specific amount of times, use a for loop with a delay: 75 | 76 | ```javascript 77 | 78 | import { delay } from 'redux-saga' 79 | 80 | function* updateApi(data) { 81 | for(let i = 0; i < 5; i++) { 82 | try { 83 | const apiResponse = yield call(apiRequest, { data }); 84 | return apiResponse; 85 | } catch(err) { 86 | if(i < 5) { 87 | yield call(delay, 2000); 88 | } 89 | } 90 | } 91 | // attempts failed after 5x2secs 92 | throw new Error('API request failed'); 93 | } 94 | 95 | export default function* updateResource() { 96 | while (true) { 97 | const { data } = yield take('UPDATE_START'); 98 | try { 99 | const apiResponse = yield call(updateApi, data); 100 | yield put({ 101 | type: 'UPDATE_SUCCESS', 102 | payload: apiResponse.body, 103 | }); 104 | } catch (error) { 105 | yield put({ 106 | type: 'UPDATE_ERROR', 107 | error 108 | }); 109 | } 110 | } 111 | } 112 | 113 | ``` 114 | 115 | In the above example the `apiRequest` will be retried for 5 times, with a delay of 2 seconds in between. After the 5th failure, the exception thrown will get caught by the parent saga, which will dispatch the `UPDATE_ERROR` action. 116 | 117 | If you want unlimited retries, then the `for` loop can be replaced with a `while (true)`. Also instead of `take` you can use `takeLatest`, so only the last request will be retried. By adding an `UPDATE_RETRY` action in the error handling, we can inform the user that the update was not successfull but it will be retried. 118 | 119 | ```javascript 120 | import { delay } from 'redux-saga' 121 | 122 | function* updateApi(data) { 123 | while (true) { 124 | try { 125 | const apiResponse = yield call(apiRequest, { data }); 126 | return apiResponse; 127 | } catch(error) { 128 | yield put({ 129 | type: 'UPDATE_RETRY', 130 | error 131 | }) 132 | yield call(delay, 2000); 133 | } 134 | } 135 | } 136 | 137 | function* updateResource({ data }) { 138 | const apiResponse = yield call(updateApi, data); 139 | yield put({ 140 | type: 'UPDATE_SUCCESS', 141 | payload: apiResponse.body, 142 | }); 143 | } 144 | 145 | export function* watchUpdateResource() { 146 | yield takeLatest('UPDATE_START', updateResource); 147 | } 148 | 149 | ``` 150 | 151 | ## Undo 152 | 153 | The ability to undo respects the user by allowing the action to happen smoothly 154 | first and foremost before assuming they don't know what they are doing. [GoodUI](https://goodui.org/#8) 155 | The [redux documentation](http://redux.js.org/docs/recipes/ImplementingUndoHistory.html) describes a 156 | robust way to implement an undo based on modifying the reducer to contain `past`, `present`, 157 | and `future` state. There is even a library [redux-undo](https://github.com/omnidan/redux-undo) that 158 | creates a higher order reducer to do most of the heavy lifting for the developer. 159 | 160 | However, this method comes with it overheard from storing references to the previous state(s) of the application. 161 | 162 | Using redux-saga's `delay` and `race` we can implement a simple, one-time undo without enhancing 163 | our reducer or storing the previous state. 164 | 165 | ```javascript 166 | import { take, put, call, spawn, race } from 'redux-saga/effects' 167 | import { delay } from 'redux-saga' 168 | import { updateThreadApi, actions } from 'somewhere' 169 | 170 | function* onArchive(action) { 171 | 172 | const { threadId } = action 173 | const undoId = `UNDO_ARCHIVE_${threadId}` 174 | 175 | const thread = { id: threadId, archived: true } 176 | 177 | // show undo UI element, and provide a key to communicate 178 | yield put(actions.showUndo(undoId)) 179 | 180 | // optimistically mark the thread as `archived` 181 | yield put(actions.updateThread(thread)) 182 | 183 | // allow the user 5 seconds to perform undo. 184 | // after 5 seconds, 'archive' will be the winner of the race-condition 185 | const { undo, archive } = yield race({ 186 | undo: take(action => action.type === 'UNDO' && action.undoId === undoId), 187 | archive: call(delay, 5000) 188 | }) 189 | 190 | // hide undo UI element, the race condition has an answer 191 | yield put(actions.hideUndo(undoId)) 192 | 193 | if (undo) { 194 | // revert thread to previous state 195 | yield put(actions.updateThread({ id: threadId, archived: false })) 196 | } else if (archive) { 197 | // make the API call to apply the changes remotely 198 | yield call(updateThreadApi, thread) 199 | } 200 | } 201 | 202 | function* main() { 203 | while (true) { 204 | // wait for an ARCHIVE_THREAD to happen 205 | const action = yield take('ARCHIVE_THREAD') 206 | // use spawn to execute onArchive in a non-blocking fashion, which also 207 | // prevents cancelation when main saga gets cancelled. 208 | // This helps us in keeping state in sync between server and client 209 | yield spawn(onArchive, action) 210 | } 211 | } 212 | ``` 213 | -------------------------------------------------------------------------------- /docs_kr/ExternalResources.md: -------------------------------------------------------------------------------- 1 | # External Resources 2 | 3 | ### Articles on Generators 4 | 5 | - [The Definitive Guide to the JavaScript Generators](http://gajus.com/blog/2/the-definitive-guide-to-the-javascript-generators) by Gajus Kuizinas 6 | - [The Basics Of ES6 Generators](https://davidwalsh.name/es6-generators) by Kyle Simpson 7 | - [ES6 generators in depth](http://www.2ality.com/2015/03/es6-generators.html) by Axel Rauschmayer 8 | 9 | ### Articles on redux-saga 10 | 11 | - [Redux nowadays: From actions creators to sagas](https://riad.blog/2015/12/28/redux-nowadays-from-actions-creators-to-sagas/) by Riad Benguella 12 | - [Managing Side Effects In React + Redux Using Sagas](http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/) by Jack Hsu 13 | - [Using redux-saga To Simplify Your Growing React Native Codebase](https://medium.com/infinite-red/using-redux-saga-to-simplify-your-growing-react-native-codebase-2b8036f650de#.7wl4wr1tk) by Steve Kellock 14 | - [Master Complex Redux Workflows with Sagas](http://konkle.us/master-complex-redux-workflows-with-sagas/) by Brandon Konkle 15 | - [Handling async in Redux with Sagas](http://wecodetheweb.com/2016/01/23/handling-async-in-redux-with-sagas/) by Niels Gerritsen 16 | - [Tips to handle Authentication in Redux](https://medium.com/@MattiaManzati/tips-to-handle-authentication-in-redux-2-introducing-redux-saga-130d6872fbe7#.g49x2gj1g) by Mattia Manzati 17 | - [Build an Image Gallery Using React, Redux and redux-saga](http://joelhooks.com/blog/2016/03/20/build-an-image-gallery-using-redux-saga/?utm_content=bufferbadc3&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer) by Joel Hooks 18 | - [Async Operations using redux saga](https://medium.com/@andresmijares25/async-operations-using-redux-saga-2ba02ae077b3#.556ey5blj) by Andrés Mijares 19 | - [Introduction to Redux Saga](https://ohyayanotherblog.ghost.io/redux-saga-clock/) by Matt Granmoe 20 | - [Vuex meets Redux-saga](https://medium.com/@xanf/vuex-meets-redux-saga-e9c6b46555e#.d4318am40) by Illya Klymov 21 | 22 | ### Addons 23 | - [redux-saga-sc](https://www.npmjs.com/package/redux-saga-sc) – Provides sagas to easily dispatch redux actions over SocketCluster websockets 24 | - [redux-form-saga](https://www.npmjs.com/package/redux-form-saga) – An action creator and saga for integrating Redux Form and Redux Saga 25 | - [redux-electron-enhancer](https://www.npmjs.com/package/redux-electron-enhancer) – Redux store which synchronizes between instances in multiple process 26 | - [eslint-plugin-redux-saga](https://www.npmjs.com/package/eslint-plugin-redux-saga) - ESLint rules that help you to write error free sagas 27 | - [redux-saga-router](https://www.npmjs.com/package/redux-saga-router) - Helper for running sagas in response to route changes. 28 | - [vuex-redux-saga](https://github.com/xanf/vuex-redux-saga) - Bridge between Vuex and Redux-Saga 29 | -------------------------------------------------------------------------------- /docs_kr/Glossary.md: -------------------------------------------------------------------------------- 1 | # 용어사전 2 | 3 | 4 | 5 | 이 문서는 리덕스 사가에 핵심 용어집입니다. 6 | 7 | 8 | 9 | ### 이펙트 10 | 11 | 12 | 13 | 이펙트는 사가의 미들웨어가 실행할 명령을 포함하고 있는 평범한 자바스크립트 객체입니다. 14 | 15 | 16 | 17 | 리덕스 사가 라이브러리를 통해 제공되는 팩토리 함수를 통해 이펙트를 만들 수 있습니다. 예를 들어 `call(myfunc, 'arg1', 'arg2')`를 사용하여 미들웨어가 `myfunc('arg1', 'arg2')`를 호출하도록 할 수 있으며, yield된 이펙트에 대한 결과는 제너레이터로 반환됩니다. 18 | 19 | 20 | 21 | ### 테스크 22 | 23 | 24 | 25 | 테스크는 백그라운드에서 실행되는 프로세스와 같습니다. 리덕스 사가 기반의 애플리케이션은 여러 테스크들을 병렬로 실행시킬 수 있습니다. `fork` 함수를 통해 이러한 테스크들을 생성할 수 있습니다. 26 | 27 | 28 | 29 | ```javascript 30 | function* saga() { 31 | ... 32 | const task = yield fork(otherSaga, ...args) 33 | ... 34 | } 35 | ``` 36 | 37 | ### 블로킹/논블로킹 호출 38 | 39 | 40 | 41 | 블로킹 호출은 Saga가 이펙트를 yield 하면 실행에 대한 결과를 기다렸다가 제네레이너 내부에서 다음 명령어의 실행을 재개합니다. 42 | 43 | 44 | 45 | 논블로킹 호출은 Saga가 이펙트를 yield한 이후 바로 실행을 재개한다는 것을 의미합니다. 46 | 47 | 48 | 49 | 예를 들어, 50 | 51 | 52 | 53 | ```javascript 54 | function* saga() { 55 | yield take(ACTION) // 블로킹: 액션을 기다립니다. 56 | yield call(ApiFn, ...args) // 블로킹: ApiFn 함수를 기다립니다. 57 | yield call(otherSaga, ...args) // 블로킹: otherSaga 가 종료될때까지 기다립니다. 58 | 59 | yield put(...) // 논블로킹: 내부 스케줄러에서 디스패치됩니다. 60 | 61 | const task = yield fork(otherSaga, ...args) // 논블로킹: otherSaga 를 기다리지 않습니다. 62 | yield cancel(task) // 논블로킹: 실행을 즉시 재개합니다. 63 | // or 64 | yield join(task) // 블로킹: task가 종료될때까지 기다립니다. 65 | } 66 | ``` 67 | 68 | ### 감시자/워커 69 | 70 | 71 | 72 | 각각 두 개의 Saga를 이용하여 제어 흐름을 구성하는 방법을 나타냅니다. 73 | 74 | 75 | 76 | - 감시자(The watcher): 디스패치된(dispatched) 액션을 관찰하고 모든 액션에 대해 워커(worker)를 포크합니다. 77 | 78 | 79 | 80 | - 워커(The worker): 액션을 처리하고 종료합니다. 81 | 82 | 83 | 84 | 예시 85 | 86 | 87 | 88 | ```javascript 89 | function* watcher() { 90 | while (true) { 91 | const action = yield take(ACTION) 92 | yield fork(worker, action.payload) 93 | } 94 | } 95 | 96 | function* worker(payload) { 97 | // ... do some stuff 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /docs_kr/README.md: -------------------------------------------------------------------------------- 1 | Redux Logo Landscape 2 | 3 | # redux-saga 4 | 5 | [![Join the chat at https://gitter.im/yelouafi/redux-saga](https://badges.gitter.im/yelouafi/redux-saga.svg)](https://gitter.im/yelouafi/redux-saga?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![npm version](https://img.shields.io/npm/v/redux-saga.svg?style=flat-square)](https://www.npmjs.com/package/redux-saga) [![CDNJS](https://img.shields.io/cdnjs/v/redux-saga.svg?style=flat-square)](https://cdnjs.com/libraries/redux-saga) 6 | [![OpenCollective](https://opencollective.com/redux-saga/backers/badge.svg)](#backers) 7 | [![OpenCollective](https://opencollective.com/redux-saga/sponsors/badge.svg)](#sponsors) 8 | 9 | ### **NOTE** 10 | 이 문서는 현재 `redux-saga` [d0aa83d](https://github.com/redux-saga/redux-saga/tree/d0aa83d782b241799264879e139db4d03438ae28) 기준으로 작성되었습니다. 11 | 12 | -- 13 | 14 | `redux-saga` 는 리액트/리덕스 애플리케이션의 사이드 이펙트, 예를 들면 데이터 fetching이나 브라우저 캐시에 접근하는 순수하지 않은 비동기 동작들을, 더 쉽고 좋게 만드는 것을 목적으로하는 라이브러리입니다. 15 | 16 | saga는 애플리케이션에서 사이드 이펙트만을 담당하는 별도의 쓰레드와 같은 것으로 보면 됩니다. `redux-saga`는 리덕스 미들웨어입니다. 따라서 앞서 말한 쓰레드가 메인 애플리케이션에서 일반적인 리덕스 액션을 통해 실행되고, 멈추고, 취소될 수 있게 합니다. 또한 모든 리덕스 애플리케이션의 상태에 접근할 수 있고 리덕스 액션 또한 dispatch 할 수 있습니다. 17 | 18 | 이 라이브러리는 비동기 흐름을 쉽게 읽고, 쓰고, 테스트 할 수 있게 도와주는 ES6의 피쳐인 Generator를 사용합니다. *(만약 Generator와 익숙하지 않다면 [여기 몇가지 소개 링크가 있습니다](https://mskims.github.io/redux-saga-in-korean/ExternalResources.html).)* Generator를 사용함으로써, 비동기 흐름은 표준 동기식 자바스크립트 코드처럼 보이게 됩니다. (`async`/`await`와 비슷한데, generator는 우리가 필요한 몇가지 훌륭한 피쳐들을 더 가지고 있습니다.) 19 | 20 | 당신은 데이터 fetching을 관리하기 위해 `redux-thunk`를 써본 적이 있을 수 있습니다. `redux-thunk`와는 대조적으로, 콜백 지옥에 빠지지 않으면서 비동기 흐름들을 쉽게 테스트할 수 있고 액션들을 순수하게 유지합니다. 21 | 22 | # 시작하기 23 | 24 | ## 설치 25 | 26 | ```sh 27 | $ npm install --save redux-saga 28 | ``` 29 | 혹은 30 | 31 | ```sh 32 | $ yarn add redux-saga 33 | ``` 34 | 35 | 다른 방법으로, HTML 페이지의 `