-
-
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
modeling side-effects #455
Comments
Giving the store control over executing side effects has some really nice benefits (see the bullet points here: https://github.com/rt2zz/redux-remotes). Something like redux-devtools would skip side-effects during replay. @gaearon based on your tweet it sounds like you are thinking reducers might provide the side effects. I imagine side effects are functions that would look something like: function sideEffect (action, dispatch){
/* do some work, fire some new actions */
} Let me know if this is in the ballpark of what you are thinking. This is the first time I have considered these concepts. |
My understanding (out of those 140 character bites) is that the sideEffect functions would (conditionally, depending on "whatever calls reducer") be executed by the reducer. If that is the case, either those side effects are supplied in an action/actionCreator, or an action would be modified by the middleware to contain those side effects (much like 'thunk-async' or the promise middleware out of issue #99 now immediately executes them). I don't immediately see on what grounds a reducer would or would not execute such supplied side effects. (as an aside, I was wondering why in #99 (comment), in the callbacks of |
@confiks next is basically dispatch, but if you call store.dispatch it will run through the entire middelware chain again, whereas if you call next it will be handed to the next middleware rather than back to the beginning. |
No, they would be (optionally) returned from it. Imagine reducer's return value is next state and, optionally, a side effect. The store may choose to invoke it or not, based on whether it's in regular flow (yes), or is doing something like replaying previously recorded actions (no). It's still very unclear to me but I share the concerns about putting too much in a middleware. I'm not sure this is the solution but it's something Elm seems to recommend, and Elm usually has great ideas, so we should definitely look into making a proof of concept. Open questions:
|
The big challenge here is the API. Returning tuples is impossible. If we can come up with a solution for how to return side effects, I will gladly put together a proof of concept. |
Perhaps with a callback? reducer(state, action, createSideEffect) |
Ya, @acdlite now that you write that out, it feels that is the only realistic way to accomplish this. But something about putting side effects in reducers feels very wrong. While strictly speaking reducers will still be deterministic since they are not executing side effects, the mental model will change. My thought is to hang this off a store as a first class concept that gets applied before the reducer. This could be achieved either in redux createStore or as a contrib store enhancer. What might that look like? For the sake of this example a function that returns side effects is called an //...
function dispatch(action) {
//...
sideEffects = currentEffector(currentState, action);
if(!isReplay){
runSideEffects(sideEffects)
}
isDispatching = true;
currentState = currentReducer(currentState, action);
//...
} Side effects would have a parallel registration process to reducers. In fact if people want to colocate their side effects they could export both a |
Ok I have read a bit more about elm side-effects and cerebral signals, and it seems to me these concepts are roughly what we are trying to achieve - but in a redux consistent way. Again I will call this macro concept Another interesting possibility here is opening up the potential for effector/reducer combination factories. For example import {createEntityHandlers} = require('redux-entity')
const {profileEffector, profileReducer} = createEntityHandlers('profile', uri)
//... later in an action creator
loadProfiles = () => ({
type: 'LOAD_PROFILES',
meta: {
entityLoader: true,
entity: 'profile',
}
}) Then based on one well modeled action, the entity effector can load data from a url and the entity reducer can receive that data. |
I made a real world example: https://github.com/gaearon/redux/tree/breaking-changes-1.0/examples/real-world Right now it uses middleware for async. Feel free to fork Redux and rewrite it with your model in mind, and let's compare benefits! |
One problem that I see is how to track side effects for cancellation. For example clicking rapidly to fetch a the same user, will trigger a lot of request that ideally will be cancelled when a new one happens. That in the world of side effects in action creators would be handled just there, but how would it be handled in this case? Is there a generic way to handle this or does it depend on the use case? If it's the latter, how does the reducer (or something else) trigger a cancellation to a previous side effect? |
I'd really like to understand this discussion because I have a feeling it is pretty important. What problem/use case is being solved? |
lol, I see this path has been travelled before @leoasis async cancellation is a hard problem (see all the troubles promises went through). I would handle it by having a stateful object available to the side effect function which can be used to cancel or take action upon an in progress side effect, basically the same as you would for action creators. @danmartinez101 the basic question is, is there a better way to organize side effects other thank using async middleware like redux-thunk. The more I think about this, the more I believe it should exist in user land. There are many possible approaches here, and it feels way to early to prescribe any one of them. The only approach that cannot be done in userland is reducer generated side-effects, but I also think that is the least exciting approach. I will continue experimentation at https://github.com/rt2zz/redux-remotes |
👍 let us know when you figure it out! |
Use Observables (e.g. Rx), that's what they do best. |
Idea of having side effects as a separate layer is quite interesting. I did some reasearch. And what we discuss here is a well-known in academia world as "Finite state machines with output". In theory they are described by two functions - transition function and output function: transition: (state, input) => state
output: (state, input) => output Technically you can combine those into one as we dsicussed: Alternatively we can do something similar to what author proposes - add another layer. But I would argue that it has to be executed AFTER reducer. There are two flavors of FSMs with output: Moore and Mealey. Mealey type: In general this approach is quite similar to React hooks. They help to deal with imperfectness and imperative nature of DOM. What we are trying to do here is quite similar. Framework then can choose to not execute any hooks on replay. The only problem is that Reducer is a function, not class, so this way of doing things is harder in Redux than it is in React. But the advantage is that such change may be non-breaking one. So it might be worth considering. |
@vladar interesting stuff. In some sense having access to |
@rt2zz actually we can develop this idea further. Whole typical React/Flux app is state machine with output, where output function is transformation of state to new React vdom. And it's Moore type of output, because React only gets new state, not intial input that caused state transition. So we can possibly treat side-effects as another type of output and model it similar to react components. Say we could have "effects" (or "tasks") that react to state changes, get (prevState, nextState) and issue side effects. Those tasks can be stateful by themeselves - similar to React which holds previous vdom state internally. But that is transient state, that shouldn't be in app state. For example in this model you can abort http requests easily; dispatch new sync actions (because it is outside of previous dispatch chain). And it will work nice with replay, if you can manage to disable effects on replay. I guess this can work with any Flux implementation and doesn't require much changes in frameworks. |
My gut is somehow telling my that just separating sync from async and thereby throwing many side effects in the same box could not be sufficient and just be a short lived solution. Quick note: I am just getting into react, rfp etc. but I wanted to share my thoughts anyway - so bare that in mind while reading. Has anyone considered a concept where you have isolated channels (per side effect/side effect groups) and each channel processes actions triggered by a message (signal/event..). Each message could trigger actions on one or more channels (or none). This separation of side effects is handled nicely in cycle.js (they call it drivers). It would add complexity, but one upside would be that each channel could have it's own "time travel strategy". I picture it like this: The most obvious channel would be called window - a combination of window.document (dom) and window.location. In redux words this would be the reducers working on the state, which in return results in (v)dom updates and maybe updates to the address bar. This channel would get the time travel strategy reproduce - meaning every action on this channel would be reproduced during time travel. Another channel would be http. This channel would obviously process async actions which would issue http request, process their responses and eventually send some messages that would be processed on the window channel. In most cases it would not make much sense to reproduce these actions during time travel, thus they would get a different time travel strategy simulate. This strategy would skip executing the actions (ignore all messages that went in), but emit all the messages that where recorded on this channel and thereby simulate this channel/side effect rather then reproducing it. Other time travel strategies could be:
This way we would get more control over side effects during time travel and new strategies could be implemented when new use cases pop up. You could even switch a channel's strategy during time travel - we all know that could result in multiple (parallel) universes but I think it could be an exciting journey and since we are talking about time travel it's something you might want to take into account. Does this make sense at all or will it just add unnecessary complexity without any real benefits? |
In the redux logger context, I don't think separating the side effects is enough. Consider disabling an action However, assume that a certain action function forward() {
service.start();
}
function backward() {
service.stop();
} When calling an action, check for attached side-effect calls and for each side-effect call the forward action. When disabling an action (or going back in history), call the backward action for each attached side-effect. I'm new to redux, and I apologize if I repeat something from above or from some other issue. |
Has anyone ever been considering using generators for side effects? Something like this:
Store enhancer should be responsible for iterating over the generator and executing all the side effects. |
Nobody seems to be interested in side effects anymore? It seems to me like the most discussed topic lately. Anyway, here's the approach described above https://github.com/salsita/redux-side-effects |
I really like Redux Saga projects which also uses generators: #1139. Feel free to contribute to that discussion! |
Relevant new discussion: #1528 |
re: https://twitter.com/dan_abramov/status/630894436810158080
I believe the idea here is, lets give the store control over executing side effects. Reducers, or reducer like things can provide side effects based on incoming actions. See http://debug.elm-lang.org/ #purity
Continuing conversation in comments...
The text was updated successfully, but these errors were encountered: