-
-
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
Reducer Composition with Effects in JavaScript #1528
Comments
I'd love this with a twist. Having the previous state to automatically go into an array through object.Assign/other invoke methods and maybe have them to be pure reducers/actions if you want them to based upon needs. Say you have a() -> 5 and b -> 7 and want c = a() + b(); ... If you want to use a later, you can reference the function as Sebastian proposed and later use it in another call by doing it from the array of yields. Sorry for explaining this very. Badly and with no code highlighting. 4 am and writing this at my iPad, will clarify tomorrow if needed |
Not sure what you mean, code would definitely help 😄 . I’ll try to put up some examples too when I find some time. |
Agreed on the awkwardness of composing |
Interesting! So would there be a new concept of effect handlers? Related reading: http://blog.paralleluniverse.co/2015/08/07/scoped-continuations/ Redux-loop seems to basically be the monadic idea. A reducer in that sense is the argument to One thing that seems weird is the use of continuations. Is it too much power to have values injected back into the continuation? Maybe it's just grating against my Redux guide training, but suddenly it seems like reducers are going to get a lot more complicated, with fetching happening in-line. But I suppose that complexity has to live somewhere. Another thought: with monadic style, you can test each phase independently, but you kind of lose that ability if you have one long thread of control. I haven't ever tried to compose separate Redux-based components, so my thoughts are all from the perspective of a monolith. More thoughts on how this would make Redux apps more composable would be helpful, to me at least. |
Hasn't anyone considered using something such as co to simulate an "effect like" operation? It looks like it could fill the current void for an effect syntax |
I use |
While co is designed to handle promises there should be a way to do the
|
We can experiment with the syntax like Elm provides. Reducer could return not only the next state but also an effect. This effect could be represented as function reducer (state, action) {
if (actionRequiresEffect(action)) {
return [nextState, IO.of(function () {
// here is the effect code
})];
}
} |
The throwing effects idea looks promising, but I have some concerns (even if such a mechanism eventually found its way into ES):
|
I implemented something quite similar to this idea. This was something that I had to come up with to solve the redux-elm-challenge. You can look at my solution here. The core of the idea is that the This is the entire code of the LocalProvider. import React, {createElement, Component} from 'react';
import { createStore, applyMiddleware } from 'redux';
const localState = (component, localReducer, mws=[]) => {
class LocalProvider extends Component {
constructor(props, context) {
super(props, context);
this.parentStore = context.store;
this.listener = this.listener.bind(this);
this.localStore = createStore(localReducer,
applyMiddleware.apply(null, [this.listener, ...mws]));
this.overrideGetState();
}
overrideGetState(){
const localGetState = this.localStore.getState;
this.localStore.getState = () => ({
...this.parentStore.getState(),
local: localGetState()
});
}
getChildContext() {
return { store: this.localStore };
}
listener() {
return (next) => (action) => {
let returnValue = next(action);
this.parentStore.dispatch(action);
return returnValue;
};
};
render() {
return createElement(component, this.props);
}
}
LocalProvider.contextTypes = {store: React.PropTypes.object};
LocalProvider.childContextTypes = {store: React.PropTypes.object};
return LocalProvider;
};
export default localState; Currently this has the disadvantage that its not possible to serialize the child/ local stores. But this is something that is easy to handle if we had some support from react itself, if it exposed the |
We also realized that the lack of fractability in Redux is a bit limiting for us to build really scalable and complex application. About a half year ago we were experimenting with changing the shape of reduction into Frankly, sometimes it's quite clumsy even in Elm because the programmer is still responsible for unwrapping the Therefore we've built https://github.com/salsita/redux-side-effects which is at least "somehow" trying to simulate the Continuations with Effect handlers approach. The idea is very simple, if reducer was generator then you could simply function* subReducer(appState = 0, { type } ) {
switch (type) {
case 'INCREMENT':
yield () => console.log('Incremented');
return appState + 1;
case 'DECREMENT':
yield () => console.log('Decremented');
return appState - 1;
default:
return appState;
}
}
function* reducer(appState, action) {
// Composition is very easy
return yield* subReducer(appState, action);
} Reducers remain pure because Effects execution is deferred and their testing is a breeze. And because I am also author of redux-elm I tried to combine those two approaches together and the result? I've ported the Elm architecture examples into Redux and was really surprised how nicely it works, there's no even need for unwrapping the reduction manually The only drawback so far is that We've been using this approach in production for almost a half year now. Seems like the perfect fit for us in terms of highly scalable architecture for non-trivial applications even in larger teams. |
@gaearon the problem you'd like to solve seems quite similar to the one here: scalable-frontend-with-elm-or-redux The problem is not clearly solved yet weither it's Elm or Redux..., and even Elm fractability alone does not help that much in my opinion. Fractal architectures often follow your DOM tree structure. So fractal architecture seems perfect to handle view state. But for which reason exactly the "effects" should also follow that structure? Why should a branch of your tree yield effects in the first place? I really start to think it's not the responsability of a branch of your view tree to decide to fetch something. That branch should only declare what has happened inside it and the fetch should happen from the outside. Clearly I don't like at all the idea of view reducers yielding effects. View reducers should remain pure functions that have a single responsability to compute view state. |
@gaearon If I understand what you're trying to solve is the boilerplate involved when adopting an Elm approach to side Effects. I'm not yet familiar with delimited continuations but from @sebmarkbage proposal what you're proposing could be something like this function A(state, action) {
// ... do some stuff
perform someEffectA
// ... continue my stuff
return newState;
}
function B(state, action) {
// ... do some stuff
perform someEffectB
// ... continue my stuff
return newState;
}
rootReducer = combineReducers({ A, B }) and then somewhere the store do something like do try {
let nextState = rootReducer(prevState, action)
prevState = nextState
runAllEffects()
}
catch effect -> [someEffectA, continuation] {
scheduleEffect(someEffectA)
continuation() // resume reducer A immediately
/*
after reducer A finishes, continue to reducer B (?)
What happens if reducer B throws here
Or perhaps the do try/catch effect should be implemented
inside combineReducers
*/
}
catch effect -> [someEffectB, continuation] {
scheduleEffect(someEffectB)
continuation() // resume reducer B immediately
} Maybe I missed something with the above example. But I think there are some issues which still has to be answered
From a more conceptual POV, AFAIK continuations (call/cc) are about providing a pretty low level way of decomposing/manipulating the flow of a program. I haven't looked in detail into delimited continuations but I guess they provide more flexibility by making possible to capture the continuation from 2 sides (having the continuation return a value). This gives far more power but also would make the flow pretty hard to reason about. My point is that for what you're aiming to achieve, it seems to me that you're using a too much heavy weapon :) I agree that redux lacks fractability, which Elm achieves by having a different way of decomposing/recomposing things (Model/Action/Update) But IMO the Elm way inevitably introduces the boilerplate of passing things down/up. And with the Effect approach it creates even more boilerplate because Elm reducers (Update) have to unwrap/rewrap all intermediate values (State, ...Effects) that bubbles up the Component tree, without mentioning the wrapping of the dispatch functions when the action tunnels down. (So I'm not sure how Elm language provides an advantage here over redux-loop besides type checking, because the boilerplate seems inherent to the approach itself). IMO fracttability could be achieved by taking the Store itself as a Unit of Composition. Dont know exactly how this would be concretely but I think this would make Redux apps composable without being opinionated on a specific approach for Side Effects and Control Flow. This way the Store has to worry only about actions and nothing elses |
Your point is certainly correct, that's definitely a drawback of fractable architecture and there still are some techniques to solve that, like for example
Why to put such a constraint to only solve
I disagree, imagine you have a list of items which you may somehow sort using Drag n Drop. Your reducer is responsible for deriving the new application state, therefore the reducer is authoritative entity to define the logic. Persisting sort order is just a side effect of the fact! In other words, saying that Event log is a single source of truth is utopia because you still need derived state to perform side effects anyway. |
Yes, the Elm Architecture is missing that piece and that's exactly where I believe Saga is very useful and needed. On the other hand using People misunderstood concept and usefulness of Sagas const dndTransaction = input$ => input$
.filter(action => action.type === Actions.BeginDrag)
.flatMap(action => {
return Observable.merge(
Observable.of({ type: Actions.Toggled, expanded: false }),
input$
.filter(action => action.type === Actions.EndDrag)
.take(1)
.map(() => ({ type: Actions.Toggled, expanded: true }))
);
}); This is still a very useful Saga for solving long running transaction, yet it's totally effectless. |
As I said on twitter, you don't need continuations here. Continuations are really powerful, and are extremely useful when you need to control the execution flow. But reducers are restricted and we know exactly how they act: they are fully synchronous, always. Sebastian's proposal is great but it's really just a more powerful generator: instead of being a shallow yield, it's a full deep yield. It's neat because you can control the execution and do stuff like this. Here we don't even call the continuation! But we don't need this with reducers. try {
otherFunction();
} catch effect -> [{ x, y }, continuation] {
if(x < 5) {
continuation(x + y);
}
console.log('aborted');
} While we could still force reducers to be synchronous (like we can with generators), there is a still a debugging cost. This is probably the main reason TC39 has been against deep yields so far: they are harder to reason about, and debugging can get painful as things jump around (they also like the sync/async interface dichotomy). But we're not going to see this native any time soon, so your only hope is compilation, but compiling this sort of stuff involves implementing true first-class continuations completely and the generated code is really complex (and slow-ish). I think it goes against the philosophy of reducers as just simple synchronous functions. You realize that with his proposal you will have to always call reducers with a try {
let state = reducer(state, action);
} catch effect -> [effect, continuation] {
continuation();
} Even if the reducer has no effects. Let me show you what I was talking about on twitter. I'm not saying this is a good idea (it may be) but if want to "throw" effects inside reducers and "catch" them outside, all while retaining the existing interface, here's what you do. You can do this with dynamic variables. JS doesn't have them, but in simple cases they can be emulated. First you create 2 new public methods that reducer modules can import: // Public methods that are statically imported
let currentEffectHandler = null;
function runEffect(effect) {
if(currentEffectHandler) {
currentEffectHandler(effect);
}
}
function catchEffects(fn, handler) {
let lastHandler = currentEffectHandler;
currentEffectHandler = handler;
let val = fn();
currentEffectHandler = lastHandler;
return val;
} We're basically making Now let's create a sample store. This does what @gaearon initially described: just pushes effects onto an array. The top-level uses let store = {
dispatch: function(action) {
let effects = [];
let val = catchEffects(() => reducer1({}, action), effect => {
effects.push(effect);
});
console.log('dispatched returned', val);
console.log('dispatched effected', effects);
}
} Now some reducers: // Sample reducers. These would do:
// `import { runEffect, catchEffects } from redux`
function reducer2(state, action) {
if(action.x > 3) {
runEffect('bar1');
runEffect('bar2');
runEffect('bar3');
}
return { y: action.x * 2 };
}
function reducer1(state, action) {
runEffect('foo');
return { x: action.x,
sub1: reducer2(state, action) };
} Both reducers initiate some effects. Here's the output of
And here's the output of
Reducers themselves can use function reducer1(state, action) {
runEffect('foo');
return { x: action.x,
sub1: catchEffects(() => reducer2(state, action),
effect => console.log('caught', effect))};
} Now the output of caught bar1
caught bar2
caught bar3
dispatched returned { x: 5, sub1: { y: 10 } }
dispatched effected [ 'foo' ] EDIT: Had some copy/pasting errors in the code, fixed |
another way of doing could be something like the Context concept of React. If we suppose that reducers are always non-method functions we can leverage the function child(state, action) {
//... do some stuff
this.perform(someEffect)
// ... continue
return newState
}
function parent(state, action) {
//... do some stuff
this.perform(someEffect)
return {
myState: ...
childState: child.call(this, state.childState,action)
}
} Similarly, catching effects from parent reducers could be done by having parents override the context passed to children function parent(state, action) {
const context = this
//... do some stuff
const childContext = context.override()
const childState = child.call(childContext, state.childState, action)
if(someCondition) {
context.perform(childContext.effects)
}
return {
myState: ...
childState
}
} We can also test the reducers simply by passing them a mock context then inspecting the mock context after they return |
@yelouafi Kind of very similiar to what I am doing in LocalProvider - see a few comments above. |
Yes, React's context is similar to dynamic variables. That changes the signature of reducers though, you can't just call them as normal functions. |
It would helpful for me at least for API ideas to include both usage and testing code examples. My guess from experience and intuition is that any test for a generator-based solution would look a lot like a test for a saga. What would tests look like using either the dynamic variable approach or the |
// Test that a reducer returns the right state (literally no difference)
let state2 = reducer(state1, action);
assertEquals(state2.x, 5);
// If you want the effects, use `catchEffects` to get them
let effects = [];
catchEffects(() => reducer(state1, action), effect => effects.push(effect));
assertEquals(effects.length, 3); Honestly this came out better than expected, I don't know how you could get much simpler and keep the existing interface 100%. (and my post above has several code examples) |
@markerikson redux-orm looks awesome, thanks! will try to find out how to fetch data into redux-orm |
I was reading this article in the last couple of days and felt that it would add to some of the prior discussions we had: http://rbcs-us.com/documents/Why-Most-Unit-Testing-is-Waste.pdf The math is truly not in favor of unit tests. I would argue that SAM with its State function can factor "safety conditions" much more easily (from TLA+, safety conditions = combination of property values the system should never reach). On another note, David Fall, created a React/Redux/Firebase SAM implementation of a dual-client Tic-Tac-Toe game (https://github.com/509dave16/sam-tic-tac-toe) |
@threepointone solved this problem with these two packages https://github.com/threepointone/redux-react-local |
This feature will take sometime to be finished. There are any temporary solution to reuse component/action/reducer today? |
@nhducit Yes, make a component with its own local Redux store. Implement that by hand. It's not hard, just create a Redux store ( You may want to use |
I created a little demo here: https://github.com/ccorcos/reduxish its basically the elm 0.16 architecture adopted ES6 classes to make everyhting a bit more JavaScripty. In terms of algebraic effect though, all we really need is for every middleware two define some kind if "wrapAction" function -- in my demo, I'm supporting wrapping redux thunk actions for example... |
We've been using Here's an example app that composes a few mini apps together: A blog post and some cleanup/additional features will be coming soon. I'd love to get some opinions on the approach if anyone has time. |
@nhducit @sompylasar I created my own lib to do these kind of things. Basically to avoid having many stores in your app. https://github.com/eloytoro/react-redux-uuid |
@gaearon Tooting my own horn here, but I think I've finally found quite an elegant solution that doesn't require any language extensions. As you've mentioned, Redux Loop comes the closest to true Elm Architecture. However, it is pretty awkward to use. So can we do better? Let's see — const createReducer = emit => (state, action) => {
if (...) {
emit(effectDescription1);
emit(effectDescription2);
return differentState;
}
return state;
} With the right What if const reducer = (state, action) => {
if (...) {
return {
state: differentState,
effects: [effect1, effect2]
};
}
return { state, effects: [] };
} ...which is basically what Redux Loop does, and is pure. The only difference is the method of communicating the effects back to the caller: return value versus sort of a side-channel. And the nice thing about this approach is that the reducer's signature remains exactly the same. Armed with this insight, I went ahead and wrote a tiny library called Petux — it's basically a store enhancer. As of this moment, it's in public beta. Any feedback would be heartily appreciated. ❤️ Here is an example of a fractal architecture you can build with it — a solution to @slorber's Scalable frontend challenge. |
@tempname11 in your modelisation of the problem, you don't prevent any developer from writing things like that and I think it's quite a big problem :) const createReducer = emit => (state, action) => {
if (...) {
emit(effectDescription1);
setInterval(() => emit(effectDescription2),1000)
return differentState;
}
return state;
} Btw don't hesitate to submit a PR |
@slorber You're absolutely right: that kind of code in a reducer is a huge red flag. But the same could be said about "vanilla" Redux, and the only solution to this problem is good communication. In Redux there is a contract between the library and its user, namely: reducers should not perform any side-effects whatsoever. With the new approach, Interestingly, with Petux, the behavior of your code sample would be entirely benign: all calls to |
I think we already discussed a similar approach somewhere, but based on generators The idea was that we could write something like: function* myReducer(state, action) => {
if (...) {
yield effectDescription1;
yield effectDescription2;
return differentState;
}
return state;
} The yielded effects are in this case the equivalent of your side channel, but by default generators will ignore effects yielded in callbacks due to their own nature. Btw I've seen you did not provide any way to combine reducers in Petux |
there's even a library doing that https://github.com/salsita/redux-side-effects |
@slorber Regarding composition: you can simply use the "vanilla" combineReducers — see https://petux-docs.surge.sh/docs/cookbook/composition.html @tomkis1 Yeah, |
@tempname11 at that point the reducer is so close to being middleware -- do you find it more convenient than middleware for some reason? |
This issue has been going on for so long, it is starting to lose context. As per the initial comment at the top, Redux was heavily inspired from Elm, but in Elm, the reducer (the "update" function actually), handles anything that involves (state, action), and actions themselves are dumb descriptions of what happened (no thunk, logic or whatever). This has the benefit that you have a single point for compositing logic. Compose update functions together, and state, effects, etc are all taken care of. In Redux, you to compose logic, you have to make sure all your middlewares are setup, setup middlewares of all the things you're composing, then compose the reducers (if selectors are not done properly the resulting shape of the state won't be useful to the UI, either), and then you have to do something about pulling in all of your side effect generators (thunks, sagas, whatever). In the Elm model, the update version is (loosely) In Elm, the model/effect are composed together and bubbled up at the top of update function stack. This is very unweildy with javascript, but also means combineReducer would no longer work. So basically, with the set of constraints imposed by the way Redux reducer composition is done, it's a very, very difficult problem (harder than the constraints Elm deal with), and that's why people like @tempname11 are trying to do stuff like that. Its not as simple as just tossing middlewares at the problem :) |
@jedwards1211 In my opinion, middleware is inherently less composable, harder to test, and harder to reason about. Largely, because it's not a pure function, like a reducer. As a thought experiment, imagine you didn't have reducers at all — just middleware that listens to incoming actions and manipulates state directly. Answering the question "what will the new state be, when this action is dispatched?" suddenly would become a lot harder. So for me, this is about making the similar — and equally important — question "what side-effects are going to happen, when this action is dispatched?" easy to answer. |
@tempname11 huh. For the apps I've been developing lately there have only ever been a few side side effects, and I mostly keep the state-affecting actions and middleware-triggering actions separate, so it hasn't been a problem for me. Back when I first joined this thread I was seeking to do something similar to what you want, but I eventually decided against that approach and figured out simpler ways to accomplish what I wanted. It sounds as though in your work you want a variety of side effects to happen for a variety of state-updating actions, so do you have some examples of why that is? |
@jedwards1211 Sure, I could give you more details and examples that led me to these conclusions, but I'm not really comfortable delving into those in this (public) thread — because it's quickly becoming more of a personal conversation. If you're interested, just drop me a line via email, and we could continue elsewhere :) |
@Phoenixmatrix you are entirely right as per the Elm 0.16 architecture, but Elm 0.17 architecture is a bit different. The problem with using thunks for side-effects is that they're opaque and introduce transient state to your application. I'll try to explain with an example. If your action is an asynchronous HTTP request that dispatches an action upon response and you press pause inside your time-traveling debugger while the request is in flight, then the response will get blocked and your application won't recover when you press play again. Thats because with this model, side-effects are not a pure function of state. Elm 0.17 introduced this concept of subscriptions which some people don't realize has the exact same type signature of the render method. The thing to realize is that rendering is an asynchronous side-effect -- people click buttons asynchronously and trigger re-renders. So what if all asynchronous side-effects were declarative? What would that look like? Here's a little example: const render = (state, dispatch) =>
state.data
? <div>{state.data}</div>
: <button onClick={() => dispatch({type: 'fetch'})}>fetch</button>
const fetch = (state, dispatch) =>
state.data
? {}
: {
url: '/blah',
method: 'GET',
onSuccess: (data) => dispatch({type: 'success', data}),
onFailure: (error) => dispatch({type: 'failure', error}),
} In the same way that React does a diff with the existing DOM and this virtual tree, we can do the same thing with HTTP requests, diffing the requests you want with all of the outstanding requests and only sending the ones that are new. Relay effectively does this. Here's a simple example of how you might build one yourself. The problem with this approach is its hard to make it performant because we're creating new function references on every re-render so we can't be lazy. There are two solution here. React deals with this by instantiating classes and your methods are bound to the class itself. This works, but its not pure functional and thus Elm can't do that. Elm did something really nifty in 0.17 where the const render = (state) =>
state.data
? <div>{state.data}</div>
: <button onClick={Action({type: 'fetch'})}>fetch</button> When you Anyways, I've studied Elm extensively and had many conversations with Evan about this stuff. If you want to check it out, I've written up some of my explorations here. I'm happy to discuss more if you have any questions. |
I think this thread has pretty well run its course. There was some very interesting discussion and intriguing ideas thrown out, but it's a big topic with a lot of nebulous ideas. Closing due to lack of actionable changes. |
Inspired by this little tweetstorm.
Problem: Side Effects and Composition
We discussed effects in Redux for ages here but this is the first time I think I see a sensible solution. Alas, it depends on a language feature that I personally won’t even hope to see in ES. But would that be a treat.
Here’s the deal. Redux Thunk and Redux Saga are relatively easy to use and have different pros and cons but neither of them composes well. What I mean by that is they both need to be “at the top” in order to work.
This is the big problem with middleware. When you compose independent apps into a single app, neither of them is “at the top”. This is where the today’s Redux paradigm breaks down—we don’t have a good way of composing side effects of independent “subapplications”.
However, this problem has been solved before! If we had a fractal solution like Elm Architecture for side effects, this would not be an issue. Redux Loop implements Elm Architecture and composes well but my opinion its API is a bit too awkward to do in JavaScript to become first-class in Redux. Mostly because it forces you to jump through the hoops to compose reducers instead of just calling functions. If a solution doesn’t work with vanilla
combineReducers()
, it won’t get into Redux core.Solution: Algebraic Effects in JavaScript
I think that what @sebmarkbage suggested in this Algebraic Effects proposal is exactly what would solve this problem for us.
If reducers could “throw” effects to the handlers up the stack (in this case, Redux store) and then continue from they left off, we would be able to implement Elm Architecture a la Redux Loop without the awkwardness.
We’d be able to use vanilla
combineReducers()
, or really, any kind of today’s reducer composition, without worrying about “losing” effects in the middle because a parent didn’t explicitly pass them up. We also would not need to turn every reducer into a generator or something like that. We can keep the simplicity of just calling functions but get the benefits of declaratively yielding the effects for the store (or other reducers! i.e.batch()
) to interpret.I don’t see any solution that is as elegant and simple as this.
It’s 2am where I live so I’m not going to put up any code samples today but you can read through the proposal, combine it with Redux Loop, and get something that I think is truly great.
Of course I don’t hold my breath for that proposal to actually get into ES but.. let’s say we could really use this feature and Redux is one of the most popular JavaScript libraries this year so maybe it’s not such a crazy feature as one might think at first 😄 .
cc people who contributed to relevant past discussions: @lukewestby @acdlite @yelouafi @threepointone @slorber @ccorcos
The text was updated successfully, but these errors were encountered: