Inconsistent Blocking Actions
See original GitHub issueThe first example in the README seems to be broken. It is supposed to wait for INCREMENT_ASYNC actions, then delay them by 1 second, and then pass an INCREMENT_COUNTER
action on, but it misses any extra actions fired in that 1 second delay.
import { take, put } from 'redux-saga'
// sagas/index.js
function* incrementAsync() {
while(true) {
// wait for each INCREMENT_ASYNC action
const nextAction = yield take(INCREMENT_ASYNC)
// delay is a sample function
// return a Promise that resolves after (ms) milliseconds
yield delay(1000)
// dispatch INCREMENT_COUNTER
yield put( increment() )
}
}
export default [incrementAsync]
This problem exists in any saga that makes a blocking call (delay
, call
, join
, put
etc.). My initial recommendation would therefore be: If you don’t want race conditions, you must never make sagas that contain blocking calls. This makes sagas pretty much useless though, unless you work on the basis of always having a top level saga that looks like while (true) yield fork(yield take('a'));
.
My recommendation is to do one of two things:
Buffer actions
One option is to buffer actions so that take
just reads the next item of the buffer. This has a couple of issues:
- It prevents parallelism. In the above example, doing
dispatch(incrementAsync());dispatch(incrementAsync());dispatch(incrementAsync());
would result in a 3 second delay (1 second * 3) rather than all running in parallel and taking about 1 second. - You don’t know in advance when or if a saga will attempt to
take
a given action type, so you are forced to buffer all actions indefinitely.
Subscribe at the top level
I imagine that 90% of sagas look like:
export default function* mySaga() {
while (true) {
let action = yield take(MY_ACTION_NAME);
// ... business logic here ...
}
}
So, with that in mind, why not just have that be re-written as:
function* mySaga(action) {
// ... business logic here ...
}
export default createSaga(MY_ACTION_NAME, mySaga);
Saga could then dispatch actions matching MY_ACTION_NAME
in parallel.
What you loose seems to be the ability to “hide” state inside sagas, but I don’t think you should be putting state inside your sagas. Keep your state in the store (updated via reducers).
Issue Analytics
- State:
- Created 8 years ago
- Comments:13 (9 by maintainers)
Top GitHub Comments
@ForbesLindesay the new release 0.8.0 introduced the helper functions
takeEvery
andtakeLatest
which offers a safer way for new users. The helpers are introduced early in the docs (README, beginner tutorial and basics section)take
is not introduced until the advanced section And Immediately followed by a section on Non blocking calls. This section illustrates the pitfalls of using take with call, and shows the right way to do non blocking calls when using take to implement complex flows.I also started refactoring the examples in the repo to use the helpers.
Do you think this would be sufficient to prevent the mentioned race issues ? New users are exposed only to the helper functions. And advanced users are warned about the use of
take
with blocking calls.Also you may find here an interesting case of how ‘missing’ the actions allows implementing some useful patterns like throttling and debouncing.
I think there are interesting ideas here, and if we can find ways to enforce that sagas are always “safe”, it will be great. As it stands, most examples in saga’s own documentation contain race conditions. I think it is safe to say if the docs for a library contain race conditions, so will real world programmes that use that library.
One possible alternative solution, instead of:
You explicitly specify up-font which action types you are interested in, which are then buffered until you
take
them. I don’t think this removes any of the flexibility you would want, but it prevents the current footgun.