Skip to content
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

Closed
gaearon opened this issue Mar 18, 2016 · 209 comments
Closed

Reducer Composition with Effects in JavaScript #1528

gaearon opened this issue Mar 18, 2016 · 209 comments

Comments

@gaearon
Copy link
Contributor

gaearon commented Mar 18, 2016

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

@evenstensberg
Copy link

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

@gaearon
Copy link
Contributor Author

gaearon commented Mar 18, 2016

Not sure what you mean, code would definitely help 😄 . I’ll try to put up some examples too when I find some time.

@lukewestby
Copy link
Contributor

Agreed on the awkwardness of composing Effects in middle reducers. The Elm compiler plays a huge role in making it doable with elm-effects. We've already begun to experiment with algebraic effects at Raise by co-opting generators to yield effects or yield spawn() other generators. I think it's about as close an approximation as we've been able to come up with without inventing syntax and writing a babel plugin. I really like @sebmarkbage's point about not needing three different execution style notations for async vs generator vs plain, and we've approximated that by just saying everything is a generator because anything might want to yield an effect at some point. We've been successful so far using it for request handlers in Node, but I'm not really sure how it would look when combined with redux-loop. I think this might be more of an evolution from redux-loop than a combination with it, what do y'all think?

@acjay
Copy link

acjay commented Mar 18, 2016

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 flatMap. The delimited continuation model seems similar to that author's scoped continuations.

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.

@eloytoro
Copy link

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

@acjay
Copy link

acjay commented Mar 18, 2016

I use co, but I'm not sure what you mean. It's basically just an option for getting async/await with generators. From the use I'm familiar with, it's not so much an effect executor as a way to work with promises. You still have to fire off the effect yourself, which is impure. In other words, you want to be able to yield some value that an effect handler will use to trigger a fetch, and inject the returned value back. But with co, you'd execute the fetch yourself and yield the resulting promise to resolve it. That's not decoupled in the same way.

@eloytoro
Copy link

While co is designed to handle promises there should be a way to do the
handling of synchronous, pure effect spawning within reducers using a
similar approach
On Mar 17, 2016 11:45 PM, "Luke Westby" notifications@github.com wrote:

^ Totally. Promises shouldn't show up anywhere in the reducer since they
necessarily represent the execution of an async function. Doesn't always
mean a side-effect occurred, but it can and usually does and that's what
we're here to avoid.


You are receiving this because you commented.
Reply to this email directly or view it on GitHub
#1528 (comment)

@Antontelesh
Copy link

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 IO monad for example.

function reducer (state, action) {
  if (actionRequiresEffect(action)) {
    return [nextState, IO.of(function () {
      // here is the effect code
    })];
  }
}

@guigrpa
Copy link

guigrpa commented Mar 18, 2016

The throwing effects idea looks promising, but I have some concerns (even if such a mechanism eventually found its way into ES):

  • Maybe the solution should not use the return channel in Sebastian's proposal (injecting values back to the reducer, even asynchronously), since IMHO that would make reducers impure or at least more difficult to reason about. The store should immediately call the continuation with no arguments whenever it catches an effect, and then process the effect asynchronously.
  • If effect handling would be built into core Redux, it would probably make it much more complex, having to handle execution of many types of effects (sync, async, parallel, serial, cancellable, etc.), just as they are now processed by redux-saga. On the other hand, if this would be left to a store enhancer we might end up with deferred continuations and more complex reducers.

@ganarajpr
Copy link

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.
https://github.com/slorber/scalable-frontend-with-elm-or-redux/tree/master/localprovider-redux-saga-ganaraj

The core of the idea is that the Provider given by React-redux is not something that has to be a single entity up the component chain. You could technically have multiple child stores down the chain but you could handle the actions they emit anywhere up the chain. This is an idea quite similar to Angular 2's child injectors.

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 _rootNodeId OR if we had a library that provided the path to the current node from the root node. I believe this is a relatively easier problem to solve eventually. But it would be quite interesting to know what others think about a solution like this.

@tomkis
Copy link
Contributor

tomkis commented Mar 18, 2016

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 Pair<AppState,List<Effects> which is basically the same like redux-loop is doing and came to exactly the same conclusion like @gaearon. It's simply too difficult to mix effectful and effectless reducers and composition becomes very clumsy.

Frankly, sometimes it's quite clumsy even in Elm because the programmer is still responsible for unwrapping the Pair manually and passing everything appropriately to sub-updaters.

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 yield side effects and return mutated app state. So the only condition is to turn all your reducers into function* and use yield* whenever you want to compose them.

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 yield* is not automatically propagated in callbacks, therefore if you want to map over some collection and yield side effects inside the mapper function then you can't. The workaround is to map over the collection and yield list of effects. Or use generator "friendly" version of map.

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.

@slorber
Copy link
Contributor

slorber commented Mar 18, 2016

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

@yelouafi
Copy link

@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

  • After catching some effect, the store has to schedule the effect for the execution later, because the reducers should be resumed immediately (reducer has still to be synchronous). Then there will be still the issue of managing control flow (long running transactions) using the state machine approach
  • How would it work if a parent reducer should (say a reducer for a router component since we aim for fractability here) catch effects from reducers of nested components. Does it have to replicate the Store catch Effect/invoke the continuation mechanism ? And if yes, wouldn't the flow of thing be a little complicated to reason about here (where the continuation frame begins and where it ends when we're in the middle of a reducer chain)
  • And there are also the issues related to Component decoupling raised by @slorber in his Elm challenge,

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

@tomkis
Copy link
Contributor

tomkis commented Mar 18, 2016

@gaearon the problem you'd like to solve seems quite similar to the one here: scalable-frontend-with-elm-or-redux

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 Action pattern matching, which as you may protest breaks encapsulation. On the other hand, from my experience this is a quite rare use case because mostly you only need direct parent <-> child communication which can be accomplished in very elegant way using The Elm Architecture.

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.

Why to put such a constraint to only solve fetch? It's about side effect in general, while fetch may potentially not be tied to the Component, there certainly are different side effects which are Component specific (DOM side effects)

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.

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.

@tomkis
Copy link
Contributor

tomkis commented Mar 18, 2016

@yelouafi

Then there will be still the issue of managing control flow (long running transactions) using the state machine approach

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 redux-saga or redux-saga-rxjs for side effects is just a side effect of the pattern itself, it solves long running transactions and Request <-> Response is long running transaction. We could say as well that Sagas should be pure and we could combine those two approaches, which IMO seems most reasonable to me.

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.

@jlongster
Copy link

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/catch effect, right? They are no longer simple functions that can be called. This is because perform only works inside the context of a try/catch effect, just like yield only works inside of function*. So in your tests calling a reducer would always be:

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 runEffect a dynamic variable. catchEffects may override this variable and run functions in different contexts. We're leveraging the JS stack to create a stack of effect handlers.

Now let's create a sample store. This does what @gaearon initially described: just pushes effects onto an array. The top-level uses catchEffects itself to gather up effects across the entire call.

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 store.dispatch({ type: 'FOO', x: 1 });:

dispatched returned { x: 1, sub1: { y: 2 } }
dispatched effected [ 'foo' ]

And here's the output of store.dispatch({ type: 'FOO', x: 5 });:

dispatched returned { x: 5, sub1: { y: 10 } }
dispatched effected [ 'foo', 'bar1', 'bar2', 'bar3' ]

Reducers themselves can use catchEffects to suppress effects:

function reducer1(state, action) {
  runEffect('foo');
  return { x: action.x,
           sub1: catchEffects(() => reducer2(state, action),
                              effect => console.log('caught', effect))};
}

Now the output of store.dispatch({ type: 'FOO', x: 5 });:

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

@yelouafi
Copy link

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 func.call method to call them within a provided context. Throwing an effect would be equivalent to invoking a this.perform(...) where this is the current context.

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

@ganarajpr
Copy link

@yelouafi Kind of very similiar to what I am doing in LocalProvider - see a few comments above.

@jlongster
Copy link

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.

@lukewestby
Copy link
Contributor

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 context approach?

@jlongster
Copy link

@lukewestby

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

@aksonov
Copy link

aksonov commented May 13, 2016

@markerikson redux-orm looks awesome, thanks! will try to find out how to fetch data into redux-orm

@jdubray
Copy link

jdubray commented May 30, 2016

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)

@sibelius
Copy link

sibelius commented Aug 2, 2016

@threepointone solved this problem with these two packages

https://github.com/threepointone/redux-react-local
https://github.com/threepointone/react-redux-saga

@nhducit
Copy link

nhducit commented Sep 9, 2016

This feature will take sometime to be finished. There are any temporary solution to reuse component/action/reducer today?

@sompylasar
Copy link

sompylasar commented Sep 9, 2016

@nhducit Yes, make a component with its own local Redux store.

Implement that by hand. It's not hard, just create a Redux store (createStore) in the component constructor and use its getState and dispatch function, and subscribe to it internally to trigger setState when the store state changes. Put the component action constants and reducers next to your component, export the action constants alongside the component.

You may want to use redux-fractal if you have already got a global Redux store in every app where you're going to use your reusable component.

@ccorcos
Copy link

ccorcos commented Nov 30, 2016

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

@mboperator
Copy link

We've been using redux-loop and a fractal architecture @ Procore Technologies for a few months now and have been loving the reusability.

Here's an example app that composes a few mini apps together:
https://github.com/mboperator/infinitely-composable-webapp-demo

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.

@eloytoro
Copy link

@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

@tempname11
Copy link

tempname11 commented Jan 11, 2017

@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 emit function, this does the trick. But is the reducer still pure? It depends. If emit itself was pure — then yes. If emit does cause observable side-effects — then no.

What if emit only collected the effect descriptions, say, into an array? Technically, that's impure. However, it would be pretty much equivalent to this:

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.

@slorber
Copy link
Contributor

slorber commented Jan 12, 2017

@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

@tempname11
Copy link

@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, emit muddles things up a tiny bit, because technically it is a minor side-effect, but we assign it the semantics of a secondary mode of output.

Interestingly, with Petux, the behavior of your code sample would be entirely benign: all calls to emit made outside of store.dispatch (including, of course, all async callbacks), are no-ops — by design.

@slorber
Copy link
Contributor

slorber commented Jan 13, 2017

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

@tomkis
Copy link
Contributor

tomkis commented Jan 13, 2017

there's even a library doing that https://github.com/salsita/redux-side-effects

@tempname11
Copy link

tempname11 commented Jan 13, 2017

@slorber Regarding composition: you can simply use the "vanilla" combineReducers — see https://petux-docs.surge.sh/docs/cookbook/composition.html

@tomkis1 Yeah, redux-side-effects has essentially the same core idea (as does redux-loop and, of course, the original Elm Arch.). Using generators in this manner is really cool from a theoretical standpoint, but I don't think it's a really practical solution: it introduces additional complexity, runtime costs and incompatibility with "vanilla" utilities like the aforementioned combineReducers.

@jedwards1211
Copy link

jedwards1211 commented Jan 13, 2017

@tempname11 at that point the reducer is so close to being middleware -- do you find it more convenient than middleware for some reason?

@Phoenixmatrix
Copy link

@jedwards1211

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) (state, action) => {state, effect}, as opposed to Redux's (state, action) => state, and that's part of why it works. So this discussion is about how we can get that effect. It isn't a hard problem and many, many people have solved it with middlewares, if you're willing to accept that composing nested reducers will require some magic (right now, in Redux, you can completely skip combineReducer. All it's doing is composing the return values of functions. If you were to return {state, effect} however, that would not longer be possible).

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

@tempname11
Copy link

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

@jedwards1211
Copy link

jedwards1211 commented Jan 16, 2017

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

@tempname11
Copy link

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

@ccorcos
Copy link

ccorcos commented Jan 17, 2017

@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 Html data type is now a Functor. What that means is we don't need to pass dispatch around anymore and what we have looks something like this:

const render = (state) => 
  state.data 
  ? <div>{state.data}</div>
  : <button onClick={Action({type: 'fetch'})}>fetch</button>

When you .map over the resulting virtualdom, you're effective mapping over the action type letting you namespace actions. The result of all of this is better performance. All of the dispatch functions get wired up for you under the hood. However, this means that you can't exactly use React anymore :(

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.

@markerikson
Copy link
Contributor

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.

@reduxjs reduxjs locked and limited conversation to collaborators Mar 12, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests