-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Alternative Approach To Async Actions #1182
Comments
Interesting approach! It seems like it should ideally be decoupled from the nature of the asynchrony, like the method used to run the XHR, or even that a web request is the source of the asychrony in the first place. |
I too have been thinking a lot about alternative ways of handling side-effects in redux and I hope I don't hijack your thread as I brain dump some of the issues I see with the some current approaches and why and how I think this is huge step in the right direction despite its apparent simplicity. The problem with side-effects in action creatorsIn pure functional languages side effects are always lifted to the edge of the application and returned to the runtime for execution. In Elm reducers return a tuple containing the new state and any effects that should be executed. Methods with this signature however are not yet composable with other redux reducers. The obvious (but possibly not the best) place to perform side-effects in redux has become the action creators and several different middleware options have been developed to support this pattern. However, I kinda think the current middleware approaches are more of a workaround for not being able to return side-effects as a first class concept of the reducers. While people are still building awesome things with redux and its a big step forward and way simpler and more pragmatic than most alternatives, there are a few problems I see with having side-effects inside action creators:
Implicit state is hiddenIn the counter application incrementAsync creates a timeout and only on its completion is the application state updated. If you wanted, for example, to display a visual indicator that an increment operation is in progress the view cannot infer this from the application state. This state is implicit and hidden. Although sometimes elegant, I'm not so sure about the proposal to use generators as orchestrators of action creators since the implicit state is hidden and cannot easily be serialized. Using redux-thunk or similar you could dispatch multiple messages to the reducer informing it when the increment operation has started and when it has completed, but this creates a different set of problems. Rewinding the state to a point where the increment operation is marked as in progress after the effect has completed will not actually regenerate the side effect and thus will remain in progress indefinitely. Your proposal appears to solve this problem. Since side-effects are created from the state the intent must be expressed with the resulting state in some form or another, thus if one reverts the store to a previous state where the action is initiated, then the reactions will initiate the effect again rather than leave the state in limbo. Duplication of business logicIt is natural for actions to generate a side effect only when the application is in a specific state. In redux if an action creator requires state it must be a simple and pure function or explicitly provided with the state. As a simple example, lets say we start with the example counter application and we want to change the counter to a random font color every time the counter is a multiple of 5. Since random number generation is impure, the suggested place to put this behavior is in the action creator. However, there are several different actions that can change the value of the counter, increment, decrement, incrementAsync, incrementIfOdd (which does not need to be modified in this case). increment and decrement previously did not require any state as they were previously handled in the reducer and thus had access to the current value, but since a reducer cannot have or return side effects (random number generation) these functions now become impure action creators that need to know the current counter value to determine if it is necessary to select a new random font color and this logic needs to be duplicated in all counter action creators. One possible alternative to explicitly providing the current state would be to use redux-thunk and return a callback to access the current state. This allows you to avoid modifying all the places actions are created to provide the current value, but it now requires the action creator to know where in the global application state the value is stored and this limits the ability to reuse the same counter multiple times within the same application or in different applications where the state may be structured differently. Context assumptions and/or dependencies reduces reusabilityAgain revisiting the counter example you'll notice there is only one counter instance. While it is trivial to have many counters on the page that view/update the same state, additional modifications to the counter are required if you want each counter to use a different state. This has been discussed before How to create a generic list as a reducer and component enhancer? If the counter used only simple action types it would be relatively trivial to apply the elm architecture. In this case the parent simply wraps the action creators or dispatcher to augment the message with any necessary context, it can then call the reducer directly with the localized state. While the React Elmish Example appears impressive, notably missing from the example are the two problematic action creators, incrementIfOdd and incrementAsync. incrementIfOdd depends on middleware to determine the current state and thus needs to know its location within application state. incrementAsync eventually directly dispatches an increment action which is not exposed to the parent component and thus cannot be wrapped with additional context. While your proposal does not directly address this problem, if incrementAsync was implemented as a simple action that changed the state to Action creators with side effects are difficult to testI think this is pretty obvious that side-effects are going to be more difficult to test. Once your side-effects become conditional upon current state and business logic they become not only more difficult but also more important to test. Your proposal enables one to easily author a test that a state transition will create a state containing the desired reactions without actually executing any of them. Reactions are also easier to test since they don't need any conditional state or business logic. Cannot be optimized or batchedA recent blog post from John A De Goes discussed the problem with opaque data types such as IO or Task for expressing effects. By using declarative descriptions of side effects rather than opaque types you have the potential to optimize or combine effects later. Thunks, promises and generators are opaque and thus optimizations such as batching and/or suppressing duplicate api calls must be handled explicitly with functions similar to Your proposal eliminates My implementationI recently created a fork of redux that allows one to create reducers that return just the new state as they do now or a special object withEffects containing the new state and a description of any effects to execute after the reducer. I was not sure how to do this without forking redux since it was necessary to modify compose and combineReducers to lift the effects over existing reducers in order to maintain compatibility with existing reducer code. Your proposal however is quite nice in that it does not require modifying redux. Additionally, I think your solution does a better job at solving the implied hidden state issue and is probably easier to combine or optimize resulting reactions. SummaryMuch like React is "just the ui", and not very prescriptive how one actually stores or updates the application state, Redux is mostly "just the store" and is not very prescriptive about how one handles side effects. I never fault anyone for being pragmatic and getting stuff done and the many contributors to redux and the middleware have enabled people to build really cool stuff faster and better than was previously possible. It is only from their contributions that we have gotten this far. So special thanks to everyone who has contributed. Redux is awesome. These are not necessary issues with Redux itself, but hopefully constructive criticisms of the current architectural patterns and the motivations and potential advantages to running effects after rather than before state modifications. |
I'm trying to understand the difference between this approach and redux-saga. I'm interested in the claim that it hides state in generators implicitly, because at first, it seems like it's doing the same thing. But I suppose that might depend on how It is an interesting concept though. Conceptualizing Redux as an action stream, from which state transitions and effects are triggered. That seems to me to be an alternative view than solely considering it a state processor. In the event sourcing model, I think it kind of boils down to whether Redux actions are "commands" (contingent requests to take an action) or "events" (atomic transitions of state, reflected in a flat view). I guess we have a tool that's flexible enough to be thought of either way. |
I, too, am a bit unsatisfied with the status quo of "smart action creators", but I've been approaching it in a different way, in which Redux is more the event store -- where actions are one of many possible effects that might be triggered by some external "controller" layer. I factored code that followed this approach into react-redux-controller, although I've got a half-baked idea in mind about a potentially lighterweight way of accomplishing this. However, it would require react-redux to have a hook that it doesn't currently have, and some store wrapping hijinks I haven't quite worked out. |
Store hijinks described #1200 |
I didn't see redux-saga until after I came up with my approach, but there are definitely some similarities. But I still some differences:
Basically, my approach emphasizes pure functions and keeping everything in the redux state. The redux-saga approach emphasizes being more expressive. I think there are pros and cons, but I like mine better. But I'm biased. |
That sounds really promising. I think it would be more compelling to see an example that factors apart the reaction machinery from the domain logic. |
As it stands, you couldn't really do that in the reactions function. The logic there would need to know which actions are already started (we can't batch anything more into them), but the reactions function does not have the information. The reactions machinery that consumes the reactions() function certainly could do those things.
I assume you mean the way in which the doReactions() function handles the starting/stopping of the XMLHttpRequest? I've been exploring different ways of doing that. The problem is that its difficult to find a generic way to detect whether two reactions are actually the same reaction. Lodash's isEqual almost works, but fails for closures. |
No, i just mean that in your example, all the configuration for setting up the concept of a reaction is mixed in with the domain logic of what data is being fetched, as well as the details of how that data is being fetched. It seems to me that the generic aspects should be factored out into something that is less coupled to the details specific to the example. |
Hmm... I think we may not mean the same thing by domain logic. The way I see it, the reactions() function encapsulates the domain logic, and is separate from the doReactions() function which handles the logic of how reactions are applied. But you seem to mean something different... |
I mostly meant that if a single event triggered a state change in which multiple components requested the same information then it might be able to optimize them. You are right however that it is not in itself sufficient to determine if a side-effect from a previous state change is still pending and thus the additional request is unnecessary. I was initially thinking maybe one could keep all state within the app state, but when I started thinking about the recent stopwatch issue I realized that while the fact that the stopwatch |
I was thinking of merging or batching requests. Eliminating duplicates should work just fine. Actually, it should handle the case of pending state changes just fine as well, since they'll still be returned from the reactions function (and thus de-dupulicated) until the server response comes back.
The way I think about it, the current pending reactions are like your react components. Technically they have some internal state, but we model them as a function of the current state. |
I kind of took the whole Then That's not to knock your method; I find this approach really appealing. |
I'm not sure react component state is a good analogy as most react state I think this kind of state is what @yelouafi refers to as control state and I think I would be less concerned about hidden saga state if sagas
|
That's totally fair.
Yes. I'm still trying to figure out the best way to split it out. It's complicated by the equality checking issue.
Sorry, I think I messed up the analogy. My point is not to compare the external action state to react component state so much as the state of the DOM. The interval or XMLHttpRequest are rather like the DOM elements that react creates and destroys. You simply tell react what the current DOM should be and make its it happen. Likewise, you simply return the set of current external reactions, and the framework cancels or starts action to make it true. |
I find that approach really interesting as well. Have you considered using multiple function main(action$) {
const state$ = action$.startWith(INITIAL_STATE).scan(reducer);
return {
DOM: state$.map(describeDOM),
HTTP: state$.map(describeRequests),
...
};
} One difference being that you don't query the drivers for events to get the action stream ( I think the React analogy works pretty well. I wouldn't consider the DOM to be the internal state though, but rather the API it works with, while the internal state is made up of the component instances and the virtual dom. Here's an idea for the API (using React; HTTP could be built like this too): // usage
const describe = (state, dispatch) => <MyComponent state={state} dispatch={dispatch} />;
const driver = createReactDOMDriver({ container } /* opts */);
store.subscribe(() => driver.update(describe(store.getState(), store.dispatch));
// (could be simplified further to eg. `store.use(driver, describe)` )
// implementation
const createReactDOMDriver = ({ container }) => {
return {
update: (element) => ReactDOM.render(element, container),
destroy: () => ReactDOM.unmountComponentAtNode(container),
};
}; |
I would have the |
I had briefly thought of it, and I'm going back and forth on it a bit right now. It makes it natural to have different reactions libraries which do different things, one for the DOM, one for http, one for timers, one for web audio, etc. Each one can do the optimizations/behavior appropriate to its own case. But it seems less helpful if you are have an app that does a bunch of one-off external actions.
I wouldn't. In my view, we want to restrict async where possible, not provide additional ways to use it. Anything you might want to call getState() for should be done in the reducer or reactions function. (But that's my purist mindset, and perhaps there is a pragmatic case for not following it.) |
Fair point. I haven't quite thought through the mapping between your idea and @taurose's example. I hastily assumed But yeah, I agree that limiting async is ideal, because if I understand the thrust of your idea, we want continuations to be pure and to map 1:1 with specific aspects in the state, like presence of an array member describing the intention that a given effect is in progress. That way it doesn't really matter if they're executed multiple times, and there's no hidden aspect of a process being stalled someplace mid-flow that other processes might implicitly depend on. |
But you're right in that it can't (shouldn't) do anything async by itself; it should leave that to the driver and just pass it some mapped state and/or callbacks.
As far as I can tell, it's pretty much the same. One difference would be that |
@winstonewert it's a long thread and I have no time to read right now or check your code but maybe @yelouafi can answer you. The redux-saga project originated from long discussions here I'm also using the saga concept for over a year on a production app, and the implementation is less expressive but not based on generators. Here are some pseudo examples I gave of the concept for redux: The implementation here is far from perfect but it just gives an idea. @yelouafi is aware of the issues inherent to using generators that hide state outside of redux, and that it's complicated to start a saga on a backend, and transmit that hidden state to the frontend for universal apps (if really needed?) The redux-saga is to redux-thunk like Free is to IO monad. The effects are declarative and not executed right now, can be introspected and are run in an interpreter (that you may customize in the future) I understand your point about hidden state inside generators. But actually is the Redux store the real source of truth of a Redux app? I don't think so. Redux records actions, and replay them. You can always replay these actions to recreate the store. The redux store is like a CQRS query view of the event log. It does not mean it has to be the only one projection of that event log. You can project the same event log in different query views, and listen for them in sagas which can manage their state with generators, global mutable objects or reducers, whatever the technology. Imho creating the saga concept with reducer is not a bad idea conceptually, and I agree with you it's a tradeof decision. |
I hope nothing I'm saying has come across as an attack on redux-saga. I was just talking about how it differed from the approach I'd come up with.
I don't really understand your point here. You seem to be arguing that a saga is a projection of the event log? But it's not. If I replay the actions, I won't get to the same place in the sagas if the saga depend on asynchronous events. It seems to me inescapable that sagas produce state which is neither in redux's state store nor a projection of the event log. |
Agreed. In principle, react could use the same interface, all event handlers would take an action creator which would get dispatched when the event fired. |
The more I think about this i think there could be a lot of synergy between this approach and sagas. I completely agree with the four points outlined by @winstonewert. I think it is a good thing that reactions cannot see user initiated actions as this prevents hidden state and ensures that business logic in reducers does not need to be duplicated in action creators or sagas. However, I realized that side effects often create non serializable state that cannot be stored in the react store, intervals, dom objects, http requests etc. sagas, rxjs, baconjs, etc are perfect for this external non serializable control state. doReactions could be replaced with a saga and the event source for sagas should be reactions not actions. |
Not at all. Ive been following the discussion but didnt want to comment without looking more closely to your code. At a first glance. It seems you only react to state changes. As I said it was a quick look. But it seems it will make implementing complex flows even harder than the elm approach (where you take both the state and the action). this means you ll have to store even more control state into the store (where app state changes alone are insufficient to infer the relevant reactions) Sure, nothing can beat pure functions. I think reducers are great for expressing state transitions but get really weird when you turn them into state machines. |
Yep. This seems to me to be the key differentiating aspect of this approach. But I wonder if this issue could be made transparent, in practice, if different effect types can be wrapped up in different "drivers"? I'm imagining it being pretty easy for people to just pick the drivers they want or write their own for novel effects. |
I'm not seeing what you are yet.
I agree. If you are hand writing a complex state-machine we have a problem. (Actually it would be neat if we could convert a generator into a reducer).
I'm not sure what you are thinking here. I can see different drivers doing different useful things, but eliminating the control state? |
@winstonewert no I'm not taking anything as an attack. I did not even had time to really look at your code :)
No I'm not, the redux store is a projection, but the saga is a plain old simple listener. The saga (also called process manager) is not a new concept, it originates from the CQRS world and has been widely used on backend systems in the past. The saga is not projection of an event log to a datastructure, it is a piece of orchestration that can listen to what is happening in your system and emit reactions, the rest is implementation details. Generally sagas are listeneing to an event-log (and maybe other external things, like time...) and may produce new commands/events. Also when you replay events in backend systems you generally disable disable side-effects triggered by sagas. A difference through is that in backend systems, the saga is often really a projection of the event log: to change its state, it has to emit events and listen to them itself. In redux-saga as it is currently implemented it would be harder do replay the event log to restore the saga state. |
Nah, not eliminating it, just making it an under-the-hood implementation concern, for most purposes. It seems to me that there's really strong consensus in the Redux community that storing domain state in the store is a huge win (otherwise, why would you be using Redux at all?). Somewhat less is the consensus that storing UI state is a win, as opposed to having it be encapsulated in components. Then there's the idea of syncing browser state in the store, like the URL (redux-simple-router) or form data. But this seems to be the final frontier, of storing the status/stage of long-running process in the store. Sorry if this is a tangent, but I think a highly general approach with good developer usability would have to have the following features:
For that second point, I think that there would have to be something pretty similar to redux-saga. It may get pretty close to what I've got in mind with its This is all kind of a tall order, but practically speaking, I think that if there are big wins to be had by having one central, serializable action record, tracking the state of an entire app at a very granular level, this would be the way to leverage it. And I think there may indeed be big wins out there. I'm imagining a much simpler way to instrument apps with user and performance analytics. I'm imagining really amazing testability, where different subsystems are coupled only through the state. I may have blown way off course now, so I'm going to leave it at that :) |
@acjay I think we agree with you on these points, the problem is to find this implementation that solves all those correctly :) But it seems hard to both have an expressive api with generators, and the possiblity to time-travel and snapshot/restore state... Maybe it would be possible to memoize effect's execution so that we can easily restore generators state... |
Not sure, but this might preclude |
As i explained in (redux-saga/redux-saga#22 (comment)) Time travelling alone (i.e. without hot reload) is possible for sagas. All you need to take a saga to a specific point is the sequence of effects yielded from the start to that point, as well as their outcome (resolve or reject). Then you'll just drive the generator with that sequence In the actual master branch (not yet released on npm). Sagas support monitoring, they dispatch all yielded effects, as well as their outcome as actions to the store; they also provide hierarchy information to trace the control flow graph. That effect log can be exploited to replay a Saga until a given point: no need to make the real api calls since the log already contains the past responses. In the repo examples, there is an example of a saga monitor (implemented as a Redux middleware). It listens to the effect log and maintains an internal tree structure (well built lazily). You can print a trace of the flow by dispatching an action Here is a capture of an effect log from the async example
|
Intriguing! And that dev tools image is awesome. |
That's cool :) |
Indeed, that saga monitor is pretty cool. Thinking about it, it seems to me that saga is solving two issues. Firstly, it handles the asynchronous effects. Secondly, it handles complex state interactions that otherwise would have required an obnoxious hand-written state machine in a reducer. My approach only tackles the first issue. I've not found a need for the second issue. Probably I haven't written enough redux code yet to run into it. |
Yeah, but I wonder if there's a way to meld the two ideas. redux-saga's That might be a whole lot of added complexity for little practical benefit. But just trying to follow this line of thinking through to the end. |
Ok, I've put together more of a library and ported the real-world example to use it: Firstly, we have the implementation of reactions: Here the example calls startReactions to enable the system: The basic configuration of reactions is here: The implementation of the github api reaction type is here: https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/api.js. This is mostly copy/paste from the middleware used in the original example. The critical point is here: https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/api.js#L79, where it uses fromPromiseFactory to create the driver from a function that returns promises. See a component specific reactions function here: https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/containers/RepoPage.js#L80. The reaction creators and common logic is in https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/data.js |
Hi folks! Raise just published a store enhancer that lets you use an Elm-architecture-like effects system as well! I hope we'll be able to learn and improve all of these approaches going forward to meet all the needs of the community 😄 |
Anyone interested in the discussion may want to see further discussion on my idea here: winstonewert/redux-reactions#7 You can also look at a branch here, where I rework the counter app to be more elmish using my pattern: I've also discovered that I'm reinventing the approach used here: https://github.com/ccorcos/elmish |
Hey @yelouafi, could you repost the link to the saga monitor idea? That is some really great stuff! The link seems to be dead(404). I would love to see more! |
Relevant new discussion: #1528 |
(I believe this is related. Sorry if that's a wrong place) Could we possibly treat all effects the same as DOM rendering?
Both View and Service layers are described via React components. And our top level (connected) components glue them together. Not sure how Is that always possible to convert an imperative API to a declarative one? Thanks |
Given we have sagas and other awesome tooling for async actions, I think we can safely close this out now. Check out #1528 for some interesting new directions (beyond just async too). |
I've been exploring an alternative to way that async actions are done in redux, and I'd appreciate any comments others might have on what I've done.
To illustrate my approach, I've changed the async example in my clone of redux: https://github.com/winstonewert/redux/tree/master/examples/async
Typically, external actions are done by making the action creators asynchronous. In the case of the async example, the fetchPosts action creator dispatch a REQUEST_POSTS action to indicate the start of the request, followed by a RECEIVE_POSTS once the posts have come back from the api.
In my example, all of the action creators are synchronous. Instead, there is a function that returns the list of asynchronous actions that should currently be taking place based on the state. See my example here: master...winstonewert:master#diff-8a94dc7aa7bdc6e5390c9216a69761f8R12
The doReactions function subscribes to the store and ensures that the actual state of requests currently being made matches the state returned by the doReactions state by starting or canceling requests.
So what's the difference?
Any thoughts?
The text was updated successfully, but these errors were encountered: