-
-
Notifications
You must be signed in to change notification settings - Fork 15.2k
An alternative side effect model based on Generators and Sagas #1139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I don't have the time to look into this closely but on a first glance this sounds very sane. Can you try porting the Redux examples in this repo to this model so we can see how it works in real apps and compare the code? |
Maybe @jlongster, @ashaffer, and @acdlite might want to take a look at this too. |
@gaearon there are already 2 examples ported : counter and shopping-cart; although there is no complex operations in the examples (added a simple example of onBoarding to the counter example; i'll try to port other examples later; it was on the roadmap anyway to provide more examples. I think we need some use cases involving non trivial operations (i mean other than simple reactions) that still can fit into small examples, perhaps some use case that someone found difficult to implement using thunks. |
Hmm...i'm not sure I understand what advantages this approach confers over say, redux-thunk or redux-effects. It seems like it's somewhere in between the two and also uses generators. (redux-gen is a composition middleware for redux-effects that does the same, and so would give you similar syntactic benefits over the Would you mind providing a comparison of redux-saga to each of these? @yelouafi EDIT: In thinking about it a bit more, it seems like the core opinion of this approach is that action creators are the wrong place for effectful logic (even if the actual effects are handled elsewhere)? |
@ashaffer the main difference as you said is that sagas are not fired in an action creator. Instead they are like daemon processes that pull the future actions. The other important difference is the broader definition of side effects. Like I said effects are more about 'things that needs to happen at a specific moment or in a specific order'. So there is no difference beteween a future action , a future timeout or the result of a dispatch call. The saga itself is an effect. So you can compose all those things together As a consequence you can implement logic that spans across multiple actions. With action creators you ll have to implement a kind of state machine and explicitly store the AC state even if it is irrelevant to the view. With generators the state is either implicit by the control flow statements or explicit but local to the saga. As redux-effects, redux-saga favors a declarative approach by separating effect creation from effect execution. But tries to not get in the way. You can use io.call(fn, ...args) to create function call effects without using the middleware handler approach. But you can also follow the redux-effects approach by yielding io.put(effectAsAction) and automatically handling the promise response from the handler middleware. You can also yield promises directly if you want. I choose to not place any restriction here and leave the choice to the developper. |
ported the async(reddit) example last night; a small but an interesting case because of concurrent requests. A quick comparison with the original thunk-based version
|
after looking at the mentioned project i'd say
But having actually discussed with @tomkis1 who already tried this approach in real production; it seems there are some issues inherent to this approach I think redux-effects and redux-saga address 2 complementary concerns. I think redux-saga can also be used as a composition middleware for redux-effects (unless I'm mistaken) because it automatically resolve responses from dispatch calls.
To be honest that was my first opinion (when i was still enthusiast about the elm model). My initial prototype used Sagas that were fired on each action; but I quickly realized that this won't allow expressing logic over multiples actions. Actually I'm less opinionated, I'm even thinking about adding support for simple Sagas that can be fired directly from Action Creators, thus providing something like redux-gen |
yield io.race([
shouldFetch ? fetchPosts(io, newReddit) : null,
true // avoid blocking on the fetchPosts call
]) seems a bit hard to read, is there a better way to express not blocking? |
I was thinking the same thing. Maybe a helper function |
I am advocating the Elm approach ever since the first time I encountered Flux and after 3 successful production projects (1 Flux, 2 Redux) I am still convinced that it's it the only right way to think about side effects and I am really convinced that I won't change my opinion in near future. My arguments are strongly based on some principal similarities between CQRS/ES and functional unidirectional data flow front end architecture (redux/flux/elm...). The way we think about CQRS/ES on the server and the client should be different because the actor is different. Computer program (client) is communicating with the server (Client<->Server architecture) and all the interactions with the system are by its nature Command based on the other hand User<->Client communication is different because user is emitting Events, things they did (very important past tense here because Command is action to be executed) with the UI, not Commands. This conceptual idea leads to the very important fact that UI is Event based, not Command based and because there are no Commands there are no Command handlers therefore all the logic should be within the reducer itself, if we treat any interaction with the UI as Event it's pretty easy, we will also get ultimate replay experience for free, because mouse clicks/moves/whatever are facts which can't simply be denied! I really believe that service and domain layer should be separated. Intention to do some side effect is business rule its actual execution is not and we can consider this some kind of service interaction. I blogged about it. And the reason that we need to reduce side effects somehow led me to the need of implementing something like redux-side-effects. |
@tomkis1
IMO Redux, despite some similarities, is not Elm architecture; in Elm a component encapsulates all its logic (render UI, update, actions) while Redux, by its nature, only encapsulates actions/update (reducer) and is totally unaware of the UI. When combined with an UI layer like React, we make a kind of Vertical Separation: the hierarchy of reducers doesn't necessarily reflect the hierarchy of components; and we establish connections at various levels between the 2 hierarchies (i.e. connect). What the Elm model provides, IMO, is a pure model to build reusable UI components (Data Grids, Dialogs, Date Pickers ...); while in the couple React-Redux, those are actually implemented as stateful React components. So if I'd compare Elm with the couple React-Redux, i'd say that the Elm model offers an alternative UI model to build Redux apps (e.g. you can build a View layer using only React pure components). But still. If by some mean we would use the Elm model to build a pure View layer for Redux; I think,and that's only an opinion, w'd do better if we separate state transitions from effect outputs. |
That, of course, if you don't consider triggering state transitions as a form of Side Effect itself |
Absolutely agree! I didn't say we should do it the same way like Elm but I was specifically talking about Elm's effect handling. It was just a reference. Anyway
I am really glad that you wrote it because it's just confirming that we shouldn't care about complex architectures (Elm) but we should rather think about simple concepts (CQRS/ES/DDD). Strictly speaking, Redux is Event Sourcing and nothing else. Now it depends only on you if you want to combine that with CQRS (side effects in action creators with
People who are claiming that Flux/Redux is CQRS are wrong because Redux is about state management and CQRS does not have anything to do with state management, it's responsibility of Event Sourcing. Your approach is interesting yet I am afraid that it suffers the same pain points like
|
Not sure if I correctely understood it; can you elaborate ? |
Could you write a unit test for this use case: When user clicks the button and condition (some flag in app state) is met, loading spinner is displayed and specific API call is executed. |
Here is my naive example: it('should display loading spinner and execute API call FOO', () => {
let {appState, effects} = unwrap(reducer({loading: false, condition: true}, {type: 'SOME_ACTION'}));
assert.isTrue(appState.loading); // loading spinner is displayed
assert.equal(effects.length, 1);
assert(effects[0].calledWith('FOO')); // specific API call was executed (remember, this is just a thunk, we don't care about implementation)
let {appState, effects} = unwrap(reducer({loading: false, condition: false}, {type: 'SOME_ACTION'}));
assert.isFalse(appState.loading); // loading spinner is not displayed
assert.equal(effects.length, 0); // No effect gets executed
}); |
what's wrong with ? it('should display loading spinner and execute API call FOO', () => {
const appState = {loading: false};
const state = reducer(appState, {type: 'SOME_ACTION'});
const effect = saga(appState, {type: 'SOME_ACTION'}).next().value;
assert.isTrue(appState.loading); // loading spinner is displayed
assert.deepEqual(effect, [apiCall, 'FOO']); // apiCall('FOO') was yielded
}); |
It's not unit test (your domain logic is in the test instead of actual code), you simply can't be sure that those two pieces which should be together: mutating the app state and some intention for side effect are actually called. A minor off-topic, but why did you use generators? In my opinion |
I understand; that's why you emphasized the world 'unit'. I can surely setup an integration test which connects the reducer and saga to some dispatcher, dispatches the action then checks the result of the 2 but of course this is not as simple as unit testing. Sure, pure functions will be always easier to test. On the other hand a separate generator makes implementing multi-step logic easier (which somewhat was the main purpose of this middleware). While in the ad-hoc reducer approach you'll have to implement some state machine for complex workflow. You'll likely also pollute your view state with some data that is only intended to manage the control flow (the memory of the 'effect driver') also the @slorber concern is to be considered.
Logic inside generators can be simply tested because you can step through yielded results using Another reason is that Generators makes other features possible; actually I'm thinking on how to express non-blocking calls (which right now could be 'hacked' using function saga(io) {
while( io.take(GET_ALL_PRODUCTS) ) {
// don't block on this call, we don't want to miss in-between events
const task = yield io.fork( fetchPosts, '/products' )
/*
if needed
const result = io.join(task)
*/
}
} |
actually the above use case was not quite expressive, you can also do function saga(io) {
while(await take(GET_ALL_PRODUCTS) ) {
// don't block on this call, we don't want to miss in-between events
const task = fetchPosts( '/products' )
// if needed
const result = await task
}
} but not things like yield io.cancel(task)
yield io.pause(task)
task.isRunning() |
added support for non blocking calls using if( shouldFetch )
yield fork( fetchPosts, newReddit ) Also got rid of the |
API is shaping up nicely, great work. 👍 |
Thanks. Happy you liked it |
@yelouafi I like what you're doing here as well. I think you are right that the most correct place for all this logic is in the middleware, so that you have a pristine log of intent. I'm excited to see how this shapes up. |
@ashaffer thanks. That's nice to hear |
Just ported the real-world example to redux-saga. main changes related to the original example
BTW updated the example to babel 6 |
And the thing i missed is ... Devtools. Which my poorman solution neither redux-router seems to handle. And which redux-simple-router |
(Sorry didnt mean to close it. Little phone screen). Only redux-simple-router seems to handle devtools time travel correctly. |
@yelouafi Yeah, we needed to handle some edge-cases in redux-simple-router to handle devtools properly. Hopefully it should work as expected. Btw, instead of this: function mapStateToProps(state) {
return {
errorMessage: state.errorMessage,
inputValue: state.router.pathname.substring(1)
}
} you could rely on the
And change the The same [here](https://github.com/yelouafi/redux-saga/blob/master/examples/real-world/containers/RepoPage.js, where you can change to: function mapStateToProps(state, props) {
const { login, name } = props.params
// ... (i.e. for most cases I don't think redux-simple-router needs to store the params — just use the |
You took my words. Saga is a great pattern for orchestrating long living transactions without need of complex state machine, you should definitely mention that in the README. Because I feel like there is some misunderstanding that people think that |
@kjbekkelund
Updating the app location seems to me much like updating the DOM with react-redux. Something that should handled automatically maybe. |
I do agree that side effects in redux is currently a little clunky, but I'm a little concerned about using generators. Generators are stateful but this state is hidden inside the generator function rather than stored within the global state object. This makes it nearly impossible to snapshot and restore the state of an application and my gut reaction tells me that storing state in generators might be a step backwards. |
That's exactly what I thought. How would you hot swap these? On Sun, Jan 3, 2016, 16:38 Kurt Harriger [email protected] wrote:
|
If you look at the redux-saga examples repo. You ll see that there is no less state in the store than in the thunk based version. Actually the state inside a saga is control state not app state. But if you want you can also store it in the store by using Actually there are arguments in favor of the 2 approaches. See redux-saga/redux-saga#8 |
@kurtharriger @despairblue this is another discussion related: redux-saga/redux-saga#13 (comment) |
Relevant new discussion: #1528 |
There were many discussions (most of them are closed some time ago) about side effects and their relations to the pure Redux model based on actions/reducers. Here i'd like to present an alternative model for Side Effects based on the ideas discussed on the older posts.
redux-saga is a middleware centered around the notion of Saga (inspired but not sure if strictly conform to the Saga pattern). So just as stated on the docs : reducers are used for state transition, and Sagas aer used to orchestrate the flow of operations.
The model tries the gather some pertinent ideas from the precedent discussions
So basically a Saga is generator function that yields effects and gets the corresponding responses. I know this was already explored and discussed, but the plus of the model is the definition of the Effects themselves: Side Effects are not simply server updates, dom storage, navigation ...etc. A side effect is anything that needs to be done imperatively at a specific point of time (i.e. is all about ordering things), so waiting for user actions, and dispatching actions to the store are also considered side effects in the model.
The point of using Generators is not simply to provide some syntactical benefits; it's that in the asynchronous world we're sill in the goto age. Nobody would say now that Structured programming just provides Syntactical benefits over the old goto style (although some people used to say that in those older times).
another important point i that Generators/Sagas are composables (either via the builtin
yield*
or the generic middleware composition mechanism) which means you can create reusable Effects and compose them with other effects (timeouts, future actions...) using parallel or race combinators.There were some side discussions on whether we should embed side effect code inside the pure code, or the inverse; I think both are 2 different views of the same principle behind side effects which is the order/interleaving of things: the application starts with a side effect, then the rest must be a well defined order of pure/effectful computation. The essential is to keep the 2 separated, and clearly define your breakpoints (actions, state updates, ...)
I'm still experimenting with the model and how to extend it, like adding monadic operations (merging, zipping or concatenating 2 Generators, just like those found in Reactive Streams). Any comments/critics are welcome of course
The text was updated successfully, but these errors were encountered: