reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.9k stars 15.27k forks source link

Alternative Approach To Async Actions #1182

Closed winstonewert closed 8 years ago

winstonewert commented 8 years ago

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: https://github.com/rackt/redux/compare/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?

1) The reactions function is a pure function of the state. This makes it easy to test. 2) The actual logic of which requests to make is simpler. See the few line function in my example, versus the various pieces of logic spread through containers and action creators before. 3) Makes it easy to cancel requests.

Any thoughts?

acjay commented 8 years ago

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.

kurtharriger commented 8 years ago

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 creators

In 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 hidden

In 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 logic

It 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 reusability

Again 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 {counter: 0, incrementAfterDelay: 1000} to trigger the side effect in a store listener then incrementAsync becomes a simple message. incrementIfOdd is pure so it could either be implemented in the reducer or could be provided the state explicitly.... Thus it becomes possible to apply the elm architecture again if desired.

Action creators with side effects are difficult to test

I 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 batched

A 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.

A Modern Architecture for FP

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 fetchPostsIfNeeded.

Your proposal eliminates fetchPostsIfNeeded and it seems totally feasible to implement a reactions function that could optimize multiple requests and/or use different set of apis as needed when more or less data has been requested.

My implementation

I 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.

Summary

Much 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.

acjay commented 8 years ago

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 io.take is implemented. If the saga will only process an action if it happens to currently be blocked at that yield, then I definitely see what you mean. But if redux-saga queues actions such that io.take will return past actions, it seems like it's doing the same thing. Either way, you've got some logic that can dispatch actions asynchronously, triggered by listening to the action stream.

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.

acjay commented 8 years ago

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.

acjay commented 8 years ago

Store hijinks described https://github.com/rackt/redux/issues/1200

winstonewert commented 8 years ago

I'm trying to understand the difference between this approach and redux-saga

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:

  1. My approach doesn't have access to the action stream, only to the state. redux-saga can start the process simply because there was an action. My approach requires that a reducer make a change to the state which triggers the reaction function to request the action.
  2. My approach requires all state to exist in redux's state. Redux-saga has the additional state that lives in the saga generator (which line it is on, the values of local variables).
  3. My approach isolates the asynchronous portion. The actual logic of the reaction can be tested without dealing with the asynchronous functionality. The saga puts these together.
  4. The saga brings different pieces of the same logic together. My approach forces you to split a saga up into portions that belong in the reducer, reactions, and reaction type implementation.

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.

acjay commented 8 years ago

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.

winstonewert commented 8 years ago

Your proposal eliminates fetchPostsIfNeeded and it seems totally feasible to implement a reactions function that could optimize multiple requests and/or use different set of apis as needed when more or less data has been requested.

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 think it would be more compelling to see an example that factors apart the reaction machinery from the domain logic.

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.

acjay commented 8 years ago

I assume you mean the way in which the doReactions() function handles the starting/stopping of the XMLHttpRequest?

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.

winstonewert commented 8 years ago

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...

kurtharriger commented 8 years ago

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 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 isOn should be stored in the application state the actual interval object associated with this stopwatch needs to be stored somewhere else. isOn should be in the app state but is is not alone is not sufficient state in this case.

winstonewert commented 8 years ago

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 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.

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 isOn should be stored in the application state the actual interval object associated with this stopwatch needs to be stored somewhere else. isOn should be in the app state but is is not alone is not sufficient state in this case.

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.

acjay commented 8 years ago

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 kind of took the whole /reactions/index module as a whole, but yeah, I would agree that the reactions function is purely domain logic. But rather than being in a domain-specific module, it's wrapped together with the boilerplate of doReactions. That's not to knock your methodology, it just makes it harder to understand at a glance the separation between library code and app code.

Then doReactions itself seems to me to be rather tightly coupled to a particular method of the particular act fetching data from an API. I would suppose that a fleshed out reactions library might a way of registering handlers for different types of effects.

That's not to knock your method; I find this approach really appealing.

kurtharriger commented 8 years ago

I'm not sure react component state is a good analogy as most react state should be in the app state, but there does apparently need to be some way to maintain state between dispatch events that cannot be placed in the store.

I think this kind of state is what @yelouafi refers to as control state and perhaps sagas is an okay way to model the non-serializable state of the system as an independent observer/actor.

I think I would be less concerned about hidden saga state if sagas responded only to application generated events (reactions) instead of user initiated events (actions) as this would allow the app reducer to use the current state and whatever conditional business logic to determine if the application should allow the desired side-effect without duplicating business logic. On Mon, Jan 4, 2016 at 5:56 PM Winston Ewert notifications@github.com wrote:

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 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.

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 isOn should be stored in the application state the actual interval object associated with this stopwatch needs to be stored somewhere else. isOn should be in the app state but is is not alone is not sufficient state in this case.

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.

— Reply to this email directly or view it on GitHub https://github.com/rackt/redux/issues/1182#issuecomment-168858051.

winstonewert commented 8 years ago

That's not to knock your methodology, it just makes it harder to understand at a glance the separation between library code and app code.

That's totally fair.

Then doReactions itself seems to me to be rather tightly coupled to a particular method of the particular act fetching data from an API. I would suppose that a fleshed out reactions library might a way of registering handlers for different types of effects.

Yes. I'm still trying to figure out the best way to split it out. It's complicated by the equality checking issue.

I'm not sure react component state is a good analogy as most react state should be in the app state, but there does apparently need to be some way to maintain state between dispatch events that cannot be placed in the store.

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.

agundermann commented 8 years ago

I find that approach really interesting as well. Have you considered using multiple doReactions, which take different state mappings? I think it would be similar to cyclejs, where you can build reusable drivers:

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 (const someEvent$ = sources.DOM.select('.class').events('click')), but specify the actions in the sink directly (<button onClick={() => dispatch(action())} />) like you have done for HTTP requests as well.

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),
  };
};
acjay commented 8 years ago

I would have the describe take getState (rather than a state snapshot) and dispatch. That way, it could be as async as it wants to be.

winstonewert commented 8 years ago

Have you considered using multiple doReactions, which take different state mappings?

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 would have the describe take getState (rather than a state snapshot) and dispatch. That way, it could be as async as it wants to be.

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.)

acjay commented 8 years ago

Fair point. I haven't quite thought through the mapping between your idea and @taurose's example. I hastily assumed describe was the reactions function, but that may not be true.

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.

agundermann commented 8 years ago

I would have the describe take getState (rather than a state snapshot) and dispatch. That way, it could be as async as it wants to be.

describe gets called on every state change, so I don't see the need for that. It doesn't mean it can't do async. Consider react components: You wouldn't call getState inside of your render methods or event handlers to get the current state, but rather read it from props.

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.

assumed describe was the reactions function, but that may not be true.

As far as I can tell, it's pretty much the same. One difference would be that reactions doesn't get dispatch. So while describe returns callbacks that create and dispatch actions, reactions returns action creators.

slorber commented 8 years ago

@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. Personally after more than 1 year of using sagas in production I don't remember any usecase where it would have been useful to be able to snapshot the state of a saga and restore it later, so I prefer expressiveness of generators even if I lose this feature.

winstonewert commented 8 years ago

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 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.

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.

winstonewert commented 8 years ago

As far as I can tell, it's pretty much the same. One difference would be that reactions doesn't get dispatch. So while describe returns callbacks that create and dispatch actions, reactions returns action creators.

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.

kurtharriger commented 8 years ago

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.

yelouafi commented 8 years ago

I hope nothing I'm saying has come across as an attack on redux-saga

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.

acjay commented 8 years ago

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)

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.

winstonewert commented 8 years ago

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.

I'm not seeing what you are yet.

I think reducers are great for expressing state transitions but get really weird when you turn them into state machines.

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).

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 sure what you are thinking here. I can see different drivers doing different useful things, but eliminating the control state?

slorber commented 8 years ago

@winstonewert no I'm not taking anything as an attack. I did not even had time to really look at your code :)

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.

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.

acjay commented 8 years ago

I'm not sure what you are thinking here. I can see different drivers doing different useful things, but eliminating the control 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 call wrappers. But a saga would have to be "fast-forwardable", in a sense, to let you deserialize it into an intermediate state.

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 :)

slorber commented 8 years ago

@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...

acjay commented 8 years ago

Not sure, but this might preclude while(true) { ... } style sagas. Would looping just be a consequence of the state progression?

yelouafi commented 8 years ago

@acjay @slorber

As i explained in (https://github.com/yelouafi/redux-saga/issues/22#issuecomment-168872101) 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 {type: 'LOG_EFFECT'} to the store

Here is a capture of an effect log from the async example

saga-log-async

Edit: sorry fixed image link

acjay commented 8 years ago

Intriguing! And that dev tools image is awesome.

slorber commented 8 years ago

That's cool :)

winstonewert commented 8 years ago

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.

acjay commented 8 years ago

Yeah, but I wonder if there's a way to meld the two ideas. redux-saga's call wrapper is a pretty simple level of indirection over an effect, but supposing you could initialize the middleware with drivers for different types of effects, you could represent them those effects as JSONable data, decoupled from the function that's actually being called. The driver would handle the detail of dispatching underlying changes of state to the store.

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.

winstonewert commented 8 years ago

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: https://github.com/winstonewert/redux-reactions/blob/master/src/index.js The interface is three functions: startReactions takes the store, a reactions function, and a mapping from names to the drivers. fromEmitter and fromPromiseFactory both create drivers.

Here the example calls startReactions to enable the system: https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/store/configureStore.dev.js#L28

The basic configuration of reactions is here: https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/index.js. The reactions function actually just iterates through the components that react router instantiates looking for ones with a reactions() function to figure out the actual needed reactions for that page.

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

lukewestby commented 8 years ago

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 :smile:

https://github.com/raisemarketplace/redux-loop

winstonewert commented 8 years ago

Anyone interested in the discussion may want to see further discussion on my idea here: https://github.com/winstonewert/redux-reactions/issues/7

You can also look at a branch here, where I rework the counter app to be more elmish using my pattern: https://github.com/winstonewert/redux-reactions/tree/elmish/examples/counter

I've also discovered that I'm reinventing the approach used here: https://github.com/ccorcos/elmish

kjr247 commented 8 years ago

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!

yelouafi commented 8 years ago

https://github.com/yelouafi/redux-saga/blob/master/examples/sagaMonitor/index.js

gaearon commented 8 years ago

Relevant new discussion: https://github.com/reactjs/redux/issues/1528

alexander-entin commented 8 years ago

(I believe this is related. Sorry if that's a wrong place)

Could we possibly treat all effects the same as DOM rendering?

  1. jQuery is a DOM driver with imperative interface. React is a DOM driver with declarative interface. So, instead of ordering: "do disable that button", we declare: "we need that button disabled" and the driver decides what DOM manipulations to do. Instead of ordering: "GET \product\123", we declare: "we need that data" and the driver decides what requests to send/cancel.
  2. We use React components as API to DOM driver. Let's use them to interface to other drivers too.
    • <button ...> - we build our View layer off "normal" React components
    • <Map ...> - we use "wrapper" components to turn imperative interface of some library to a declarative one. We use them the same way as "normal" components, but internally the are actually drivers.
    • <Chart ...> - this could be either of the above depending on implementation. So, the line between "normal" components and drivers is blurred already.
    • <Http url={'/product/'+props.selectedProductId} onSuccess={props.PRODUCT_LOADED} /> (or "smart" <Service...>) - we build our Service layer off (UI-less) driver components

Both View and Service layers are described via React components. And our top level (connected) components glue them together. This way our reducers remain pure and we do not introduce any new means to handle effects.

Not sure how new Date or Math.random fit here.

Is that always possible to convert an imperative API to a declarative one? Do you think this is a viable view at all?

Thanks

timdorr commented 8 years ago

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).