reduxjs / redux

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

Reducer Composition with Effects in JavaScript #1528

Closed gaearon closed 7 years ago

gaearon commented 8 years ago

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


cc people who contributed to relevant past discussions: @lukewestby @acdlite @yelouafi @threepointone @slorber @ccorcos

tomkis commented 8 years ago

For me your reducer should just describe that the order has changed

That's what I am talking about, if State is derived in reducer and API call clearly depends on the derived State, why can't we treat the persistence of the derived state as side effect of the derivation? Or maybe you can enlighten me what's the benefit of delegating that responsibility to saga? Because I can clearly see the Saga here as effect executor which IMO is not main purpose of Saga.

ghost commented 8 years ago

I'm going to play devil's advocate here and use redux to poke holes in the broader FRP approach to constructing interfaces. I have been on a Rich Hickey kick lately and am going to hammer his point of simple versus easy. I really would like people to share their thoughts and (hopefully) provide some meaningful counterpoints.

...

In the beginning everything was easy. We had state machines driven by top-down, event-based actions that afford easily-traceable control flow. So long as we stayed inside the browser and only listened to user events, this would have been a utopia. But how do we actually communicate any async behavior via ajax/websockets, animations, etc.?

I started with redux-thunk as an easy, no-nonsense way to preform async behavior at the edges of a sync application architecture. After some debate, we decided that thunks are pretty unpredictable, and lack larger control-flow semantics, making it nearly impossible to coordinate multiple components. There was no in-house, direct solution to "component A did something, sibling component B now needs to go do something too" that didn't involve tightly coupling (de-simplifying) A and B.

I found neat libraries like redux-effect and redux-saga (thanks @yelouafi). Having some familiarity with sagas from a DDD perspective, the expressiveness and control flow power of redux-saga was a no-brainer. Now we could coordinate long-running transactions and keep unrelated components decoupled.

But then reusability issues cropped up as the application grew. How do we have dynamic lists of components? How do we reuse components in different places across the app? I started looking for inspiration further afield in FRP conceptualizations like Cyclejs and various flavors of The Elm Architecture. Each one of these approaches "solved" reusability, but also introduced some complexity (I mean this in the "braided-together" sense of the word, not "difficult to understand initially").

It seems like no one has landed on a solution that allows you to build a completely independent component packaged up with its own set of actions, a reducer, and perhaps some side effects, that can be used as-is from the outside world without placing an implementation burden on the component. @slorber is also still on the hunt for a solution to this problem.

...

If I take a step back and ponder the whole predicament: what am I trying to accomplish? I want to build simple (not braided together) interfaces that deliver business value. Simplicity at the minimum affords testability, extensibility and modifiability. The more testable, extensible, and modifiable an application the easier it is to satisfy business demands. An FRP approach helps in what ways?

1. It is relatively easy to keep state in sync, but is it simple?

Using reducers and "data as a first class citizen" is drastically easier and simpler than a lot of other techniques such as object encapsulation, getters/setters, ORMs, etc., but is it as simple as it could be? It's very easy to fall into a "event and handler soup" pattern with FRP where parent components have intricate knowledge of child components (coupled), or child components have knowledge of events outside of their domain (unscalable), or parent components are translating one event into another event that their child can understand (tedious, error prone).

2. Replayability or "global undo" is essentially free

... until it isn't. The moment we introduce side-effects, generators, sagas, loops, or thingamabobs "perfect" time travel becomes either unachievable or places an implementation burden on every single component. How many apps truly need a global undo versus a simple local undo?

A global atom is a great idea because it can be harnessed as a front-end database, and we have been using relational databases for a very long time to build wonderfully simple things. However, I don't think that the redux flavor of reading/writing to this database is a simple as it can be.

3. What other advantages, if any?

No, seriously, I'm trying to figure out what else we are getting by trading away the aforementioned.

ccorcos commented 8 years ago

@aft-luke in terms of replayability, I've been playing around with this elmish concept, very similar to redux, but all side-effects are handled declaratively in the the render function. One of the best things about this approach is everything is replayable even with side-effects! And since side-effects are declarative, the app is recoverable even when you the app while the side-effects are in flight (without any hacks)! I hope you like it -- I could use some help bouncing ideas around.

lukewestby commented 8 years ago

or parent components are translating one event into another event that their child can understand (tedious, error prone)

I think this is how Elm succeeds with the Elm architecture insofar as it does. Translation of parent actions into child actions is still tedious, but not error prone. I don't know how to satisfactorily eliminate those errors in JavaScript

doodledood commented 8 years ago

So I sat down yesterday and borrowed a lot of ideas from all around to create a new effects library which is based on the Elm Architecture, that solves some of the issues discussed above. I think the API came out quite nicely, although the implementation is quite hacky at the moment and is tightly coupled to Redux's implementation.

You can find it here (redux-fx)

Please tell me what you think about it.

You can use it basically like this:

  1. First you enhance the createStore with the provided enhancer:
const createStoreWithMiddleware = compose(
  applyMiddleware(someMiddleware),
  enhanceStoreWithEffects(),
  devTools()
)(createStore);
  1. Then you export your reducer wrapped in the higher order function "withFx" and you can now write your reducers Elm-style like so (using "fx" function to create an effect descriptor):
const incrementWithDelay = seconds => dispatch => setTimeout(() => dispatch({type: "INCREMENT"}), seconds * 1000);

const reducer = withFx((state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return [{count: state.count + 1}, fx(incrementWithDelay, 1), fx(incrementWithDelay, 2)];
    case "DECREMENT":
      return {count: state.count - 1};
    default:
      return state;
  }
});

What happens is basically "withFx" is putting makeup on the reducer so you can write it Elm-style but under the hood, strips it back to a normal reducer. The effect descriptors then "bubble up" and are executed after the new state has been reduced.

The neat thing is that you can also provider your own "effects runner" and support other types of effects or other ways to execute them or not even run them at all.

lukewestby commented 8 years ago

@doodledood would you be interested in integrating some of the ideas you've come up with into redux-loop?

doodledood commented 8 years ago

@lukewestby I thought about doing excatly that but that would actually be a complete rewrite of most of the library so I just opened a new one because it was easier

lukewestby commented 8 years ago

@doodledood that doesn't seem like a big deal to me, version numbers are free :smile: If not, would it be okay if we incorporate some of your ideas to improve redux-loop on our own?

doodledood commented 8 years ago

@lukewestby sure go ahead :) this is why its called open source

sompylasar commented 8 years ago

@doodledood @lukewestby Funny thing, I'm currently experimenting with functional effects on the project I'm working on (haven't integrated Redux yet, but experimental mini-redux instances are handwritten in a few components that urge for a state management). I decided to go from scratch to check what I can come up with starting off my own requirements, and the result came naturally as I was writing these things. The code I'm going to show is very fresh, it's only been written today, hasn't run it yet, so there could be errors.

There are several differences to redux-fx. Feedback is very welcome, thanks in advance.

As the effects' functions can't be serialized, I didn't try to make the effect descriptor serializable (the { type: "FX" } thing) -- I made it a constructed instance that can be type-checked with the instanceof operator.

    var NO_EFFECTS = [];
    function WithEffects(state, effects) {
      this.state = state;
      this.effects = effects;
    }
    function withEffects(state, effects) {
      return (effects ? new WithEffects(state, effects) : state);
    }
    function withEffectsGetState(withEffectsObj) {
      return (withEffectsObj instanceof WithEffects ? withEffectsObj.state : withEffectsObj);
    }
    function withEffectsGetEffects(withEffectsObj) {
      return (withEffectsObj instanceof WithEffects ? withEffectsObj.effects : NO_EFFECTS);
    }

I use it like follows (sorry, no Babel yet, lodashExtend should be object spread ... in my dreams):

    function reducer(state, action) {
      switch (action.type) {
        case ACTION_SOMETHING_HAPPENED:
          return withEffects(
            lodashExtend({}, state, {
              _state: STATE_SOMETHING_GOING_ON,
            }),
            [
              somethingHappenedEffect(state, {
                delay: action.delay,
              }),
            ]
          );
        default:
          return state;
      }
    }

    function somethingHappenedEffect(state, options) {
      return function (dispatch) {
        setTimeout(function () {
          dispatch({
            type: ACTION_SOMETHING_HAPPENED,
            delay: 1000,
          });
        }, options.delay);
      };
    }

And here's my mini-redux-with-effects implementation:

    var store = (function createStore(initialState, reducer) {
      var _state = initialState;

      function getState() {
        return _state;
      }

      function dispatch(action) {
        var withEffectsObj = reducer(_state, action);
        var nextState = withEffectsGetState(withEffectsObj);
        var nextEffects = withEffectsGetEffects(withEffectsObj);
        if (nextState !== _state) {
          // Here could be logging or calling an `onChange` to re-render, hence the `if`.
          _state = nextState;
        }
        if (nextEffects && nextEffects.length) {
          // The effects are made synchronous by default for a reason.
          // This would serve for a web video player that requires
          // to start playback from a user-gesture call-stack,
          // but starting playback is an effect of an action.
          var effect;
          while ((effect = nextEffects.shift())) {
            effect(dispatch);
          }
        }
      }

      return {
        getState: getState,
        dispatch: dispatch,
      };
    }({
      _state: STATE_INITIAL,
    }, reducer);

    store.dispatch({
      type: ACTION_SOMETHING_HAPPENED,
      delay: 5000,
    });

Thanks again for any feedback.

sompylasar commented 8 years ago

@doodledood What do you think about: the effect captures the current state at the moment it was created (my approach), or the effect uses getState to get the state at the moment it is executed (your approach)?

doodledood commented 8 years ago

@sompylasar Well, the getState I supplied the effect with is just a bonus enabling stuff like long running tasks and timers. The idea was to capture state using args when constructing the effect descriptor and then using that to run the effect

sompylasar commented 8 years ago

@doodledood Yes, makes sense. I tried passing the whole store (i.e the pair of { dispatch, setState }), but then I discovered that this requires me to write var prevState = store.getState() at the effect factory or at the effect start. I don't have long-running effects in this code yet, so probably for a more generic (library) case getState should be passed to the effect instead of just the current state snapshot, or along with it.

I can even modify like that:

    function somethingHappenedEffect(state, options) {
      return function (dispatch, getState) {

and

          var effect;
          while ((effect = nextEffects.shift())) {
            effect(dispatch, getState);
          }

(and just realized this while loop should've been a lodashEach to avoid mutating the effects array).

doodledood commented 8 years ago

@lukewestby @sompylasar Ok, I wanted to take this a little further - so I reimplemented redux-fx from scratch to add support for advanced use cases.

What I gained by doing this was:

The funny thing is, without noticing, what I wrote was really similar to redux-loop except some implementation details and a few features, so maybe we should integrate those changes into redux-loop.

Most, if not all of the exercises on elm-architecture-tutorial can be implemented using this idea quite nicely. I have implemented something similar here .

@slorber @gaearon Can you guys take a look sometime and tell me what you think?

ms88privat commented 8 years ago

My first blog entry is live! https://medium.com/@marcelschulze/using-react-native-with-redux-and-redux-saga-a-new-proposal-ba71f151546f#.vlqki5ca6

My input on managing AppState/Side Effects with Redux and Redux-Saga

Any feedback is very welcome. Thanls

ccorcos commented 8 years ago

Interesting, so your side-effect basically shortcut to be global.

One thing I always had in mind that now wouldnt work is if you could encode graphql queries and as an "effect" and compose them to the top level. Just an idea

jonaskello commented 8 years ago

@ms88privat I've started using redux-saga recently and have also seen this pattern. For example, I have a treeview that issues actions that the user does (expandClicked, nodeSelected, nodeDeleteRequested etc.). Then I have a saga that listens to those actions and issues reducer actions. I think this is applying the pattern you describe on a Widget level. Eg. if you have Widget A, it can issue actions for what the user does, then you have Saga A that watches actions and does side effects and issues actions that alter state. This pattern came naturally for the treeview but I have not formalised it in other parts of the app as I am not sure of it's drawbacks. It feels like it turns user events into commands in the CQRS sense. The obvious advantage being decoupling so that the Widgets are "dumb". On the other hand it might just be unnecessary indirection if the widgets are not re-usable outside of one specific app?

ms88privat commented 8 years ago

@jonaskello Thank you for your feedback. It may be unnecessary, but for me the readability is most important. Without the mapping of User-Actions to Reducer-Actions, you can only describe one side of the interaction. E.g. the Action expresses what happened, or will happen, but not both.

In addition you have all your descriptive logic in one place (saga) and you can easily add new features.

jonaskello commented 8 years ago

@ms88privat I like the pattern but I also remember reading this discussion and became a bit worried it is too imperative. Anyway, I think we are de-railing this thread. Maybe you should open a separate discussion about the user-action/reducer-action pattern on the redux-saga repo?

doodledood commented 8 years ago

@ccorcos Right, but also I don't think that a graphql query description should live inside a reducer as effects. I think data needs should live inside the components themselves and only the execution part of the query should be an effect, and that can be implemented using this method

jdubray commented 8 years ago

@ms88privat The way you mutate state is not something you make up, it follows a well defined pattern which is best described by the Paxos protocol: Propose, Accept and Learn. Again, this is not something that you are free to design. There could be a class of problems where you could find shortcuts, but this seems not to be the case of Front-End architectures.

The fundamental flaw in redux is to believe that the reducer can accept anything that's presented to it. When you persist the application state (as derived from the reducer) there is a great chance that the persistence layer will respond sometimes in such a way that the proposed values (from the actions) cannot be accepted. Trying to frontload or backload your business logic in such a way that all proposed values must be accepted at all times is simply the absolute worse factoring you could think about because the logic to deal with exceptions will show up everywhere except where it belongs, in the acceptor code.

When you combine with the fact that Sagas break the principle #1 in redux (a single state tree), the question is what are you trying to achieve? Could you at least look at the theory behind state mutation and align with it? then, if there are any optimizations, I am certain really smart people will find them.

slorber commented 8 years ago

@jdubray i've read some of your stuff and it's hard to follow. I've asked you on twitter to submit a solution for scalable-frontend-with-elm-or-redux but did not get any answer nor pull request. It's hard to judge on your ideas if you don't show any code that we can run and that actually solves concrete problems.

What I can say from reading you is that I don't understand at all what can be the relationship between a quorum protocol and a frontend technology where all the code runs on the browser.

Paxos is a family of protocols for solving consensus in a network of unreliable processors

Until you introduce communication with a remote service, I don't see any network of unreliable processors in Redux apps. I don't see any mention of such communication in your post (except maybe the word "persist" which may represent that?)

If no network communication, what I understand is that there is only a single acceptor. And as it does not make sense to present the user a button if he's not allowed to click it, then generally the acceptor will just accept any action (because otherwise we wouldn't put a onclick listener on that button, or even better not show the button at all)

If network communication, it's likely that it's with a single backend (at least for API-based apps) and that the backend takes the decision to accept the state mutation. So in this case I don't see the point of using a quorum (at least between the frontend and backend, but a cluster of backends can use quorums if needed)

iazel commented 8 years ago

Hello everyone, I've read the entire topic and would like to propose my solution to you. It's really, really simple but I think it could be a nice addition to the stack, couldn't it? I'm not sure if it's the correct name, but for now I called it the taskMiddleware. It's implementation is, again, nothing complex:

(reduce) => ({ dispatch, getState }) => (next) => (action) => (
  (action.meta.task) ? reduce(dispatch, action, getState()) : next(action)
)

It essentially introduce a second reducer where all our business logic will reside, it should be a pure function that cannot modify the state directly but will dispatch action accordingly. As you can see from it's definition, it listen for any action with .meta.task. I think this is the correct way to follow for middlewares, even better if using Symbol.

Consider the Counter example with async and "if odd" action, it will be implemented like this:

import * as A from './actions'

// this is our normal, dumb, state reducer 
export function stateReducer(state, action) {
  if(state === undefined)
    state = 0

  switch(action.type) {
    case A.INCREMENT:
      return state + 1

    case A.DECREMENT:
      return state - 1

    default:
      return state
  }
}

// in here will reside all of our business logic
export function taskReducer(dispatch, action, state) {
  switch(action.type) {
    case A.INCIFODD:
      if(state % 2 === 1)
        dispatch( A.increment() )
      break

    case A.INCASYNC:
      setTimeout(() => dispatch( A.increment() ), action.payload)
      break
  }
}

As you can see, it's quite easy to reason about and it's fully composable, in fact you can see this in action in a small repo I've created just today Deku-Redux Experiments.

I've realized a generic List component (with some hacks on Deku) but what it's important here is this part:

// https://github.com/Iazel/deku-redux-experiments/blob/master/list-counters/componenets/list/reducers.js#L33
const createTaskReducer = (reduce) => (dispatch, action, state) => {
  if(action.type !== A.ITEM_ACTION)
    return

  const { index, item_action } = action.payload
  reduce( listDispatch(index)(dispatch), item_action, state.get(index) )
}

Then I can use it like this:

const taskReducer = createTaskReducer(item.taskReducer)

Done. Inside the taskReducer you can do anything you want, and even build some other abstraction upon it. In this particular case, createTaskReducer and the List itself has no knowledge about Counter and viceversa. Side note: listDispatch wrap dispatch to tap in the action flow of the child item.

The store creation is nothing more of the standard one:

const store = createStore(stateReducer, initialState, applyMiddleware(
  taskMiddleware(taskReducer)
))

Because taskReducer is a pure simple function, we pass anything it needs and because its action are the standard Flux one, it should be quite straightforward to test. Tell me what you think about it, especially what I'm doing wrong! Thanks :)

slorber commented 8 years ago

@Iazel taskReducer is not pure if it calls dispatch or setTimeout

Not sure but it seems your proposal is quite close these SO answers: http://stackoverflow.com/a/33829400/82609 http://stackoverflow.com/a/33501899/82609

iazel commented 8 years ago

@slorber Well, you're right, I misused the term xD What I meant is that this reducer is still easily composable and will produce a result depending solely on it's input... More or less... Well, I've fixed it xD I was thinking just yesterday if, for complex applications, is it ok to introduce another layer for the side effects, given a total of 3 reducers: state, logic/coordinator (really a pure function this time) and side effects. This kind of technique seems quite flexible to me.

That's said, what kind of use case could you think about where other solutions (e.g. redux-saga, redux-elm, etc.) are more suited for the job? I actually started a new project, my first one with redux, and because it has the potential to be quite complex, I would really like to get it right :)

tihonove commented 8 years ago

Hi everyone,

I've spent many time to find solution for side effect composition. Idea to combine 'Algebraic Effects' and Redux-loop is very impressive. Another point: i love redux-saga, because it allows to write simple imperative-like code for business logic. Few days ago, I write library that solves all my problems (IMHO).

The main idea: reducer should returs a pair { state, effects } like redux loop, but effects should be a generator like redux-saga. Effects generator returns plain objects exactly like actions. Here sample code:

function reducer(state, action)  {
    if (action == 'Action1') {
        return effected(state, function* {
            yield put({ type: 'AnotherAction' });
            yield take(x => x.type === 'AnotherAction2');
        })
    }
    return state;
}

In nature, effects is a stream of actions that bubbles up through composed reducers and can be transformed. For example:

function reducer(state, action)  {
    if (action == 'Nested.Action1') {

        var [newState, effects] = splitEffected(nestedReducer(state, { ...action, type: action.type.replace(/^Nested\./, '') }))

        return effected(newState, effects::map(effect => {
            if (effect.type === 'PUT') {
                return { ...effect, action: { ...effect.action, type: 'Nested.' + effect.action.type } };
            }
            return effect;
        })
    }
    return state;
}

Another example: bubbling side effects and can be handled at any level of reducer composition:

function nestedReducer(state, action) {
    //...
    if (action == 'Action1') {
        return effected(state, function* () {
            var randomNumber = yield { type: 'GenerateRandomNumber', min: 10, max: 100 };
            yield put({ type: 'UpdateNestedValue', value: randomNumber });
        })
    }
    //...
}

function reducer(state, action)  {
    if (action == 'Action1') {

        var [newState, effects] = splitEffected(nestedReducer(state, action)

        return effected[newState, effects::map(effect => {
            if (effect.type === 'GenerateRandomNumber') {
                return call(function* () {
                    return getRandomInt(effect.min, effect.max)
                });
            }
            return effect;
        })
    }
    return state;
}

I have spent much time to solve problem of easy to reuse 'Confirmation dialog'. This approach solves it easily:

function nestedReducer(state, action) {
    //...
    // I want to delete item with confirmation
    if (action == 'ConfirmedDelete') {
        return effected(state, function* () {
            var confirmed = yield { type: 'Confirm', text: 'Delete item?' };
            if (confirmed)
                yield put({ type: 'Delete', id: action.itemId });
        })
    }
    //...
}

function reducer(state, action)  {
    //...
    var [newState, effects] = splitEffected(nestedReducer(state, action))
    return effected(newState, effects::map(effect => {
        if (effect.type === 'Confirm') {
            return call(function* () {
                yield put({ type: 'ConfirmDialog.Show', text: effect.text });
                var confirmOrDiscard = yield take(x => x.type === 'ConfirmDialog.Discard' || x.type === 'ConfirmDialog.Confirm');
                if (confirmOrDiscard.type === 'ConfirmDialog.Confirm')
                    return true;
                return false;
            });
        }
        return effect;
    })
    //...
}

I create repository with enhancer for createStore that allow to write reducers like examples above. https://github.com/tihonove/reelm

Plese consider this approach and say 'dead' or 'can live'. I'am a newbie in front-end development and spent many time to write readable code, so this approach is 'can live’, I ready to provide more examples and recipes.

JohnNilsson commented 8 years ago

Why do we want the reducers to provide side effects? I take effect here to mean an imperative description of something to be done, it kind of implies that the reducers need to decide exactly how some desired goal is to be achieved.

In some sense it seems like we're trying to recreate the problem React so elegantly solved for the DOM update problem. And I think the same solution is probably desirable in this case.

What if, instead of focusing on the effects, we put the focus on the demands that make the effects wanted? Just as there's some sentiment that derived data should be handled by selectors and not reducers, effects can be considered derived from demands. By keeping actual effects open to interpretation as long as demands are met we gain some freedom in how to satisfy those demands. An instance of that might be different interpretations in browser, in server and in test environments.

Someone mentioned above that queries should be put in the sate tree, which seems to be in that direction. Consider a relational database, it separates the demand (SQL) from the effect (executing the query) by a pipeline of interpreters, validators and optimizers.

In redux the example might be data demand. Clicking the button changes the application state into one demanding data to be loaded, or navigation state to change, or DOM to change. The DOM demand is not stored in the redux store, bu instead derived through things like react components computing a vdom, and then the effects of updating the DOM is derived from the VDOM using the diff engine. Similarily the data demand might actually be derived from the navigation demand (think angular-ui-router and it's resolve), and the effects what to fetch, how to fetch, and such, derived from that using some data diff engine.

So I guess I'm saying that state, and transitions between states, should be kept high-level and domain specific, and any effects should be derived from chains of meaning ( http://www.vpri.org/pdf/tr2009016_steps09.pdf )

Also speaking of "demands" there might be some insights available from https://awelonblue.wordpress.com/ a lot of thinking about a concept called Reactive Demand Programming ( https://github.com/dmbarbour/Sirea ) : "In RDP, the only effect is observing of the active set of demands by a resource"

acjay commented 8 years ago

@JohnNilsson I believe you're echoing a point I made earlier in the thread: https://github.com/reactjs/redux/issues/1528#issuecomment-198710920

JohnNilsson commented 8 years ago

Indeed, I think I am. Perhaps a slight variation is where to keep the fetches state. In one sense I think fetch is too low level for state, something identifying a graphql query is probably more what we're after. Also I'm seeing the fetches data structure being diffed against some other state, not in the store, things like promises representing outstanding requests f.ex. Consider the typical rx-example of switching out the demand for an incremental search. A good exercise would probably be to implement twitters bloodhound engine like this.

acjay commented 8 years ago

Yeah, in my mind, the representation is arbitrary. If there's a store listener that understands fetch demands phrased in GraphQL, so much the better!

And yep, there would be external state in the form of whatever is doing the fetching. I do think the idea would be limited if the store's state didn't also reflect things in progress. That would make it difficult to differentiate between absent data and data actively being loaded in the UI. And it would be difficult to account for data that is present, but stale. In some cases, like incremental search, I agree that the store's representation could be simplified to omit the state of fetching.

jdubray commented 8 years ago

it separates the demand (SQL) from the effect (executing the query) by a pipeline of interpreters, validators and optimizers.

This is not the correct way to look at it, the sole purpose of SQL is to propose the new state without the knowledge of the rules that would make it acceptable. SQL proposes values to the acceptor which will validate (constraints), enrich (resolve indices) and optimize (plan) the mutation of the state.

Thinking in terms of demand/effect is simply the wrong way to look at it. I am not quite sure why you guys absolutely refuse to look at the theory behind mutating state. It's like trying to build an airplane and ignoring gravity. Do you really think that all this is arbitrary and you can design your way out of this problem? That's pretty naive if you ask me.

acjay commented 8 years ago

@jdubray, I agree with @slorber that if you do indeed have a better model for managing state and mutations, your descriptions haven't quite sold it. I'm sure I'm not alone in adopting Redux not just because it is a great piece of software, but also because the guide very thoroughly motivates the concepts behind the library. It's pretty clear to me that no one here is trying to ignore great ideas. I challenge you to rethink how your present your ideas.

jdubray commented 8 years ago

This is not "my" description. I am simply encouraging people to look at the theory behind mutation. After all, it has only got a Turing award for it, so it's probably not worth considering in this group. Here is the book written by Dr. Lamport on TLA+ http://research.microsoft.com/en-us/um/people/lamport/tla/book-02-08-08.pdf

SAM was founded on the principles of TLA+. See for instance page 34 (please click on the embedded links:

HCSpec expresses both how the model mutates (next-state relation) and the next-action predicate. This is coming from the [] operator which is not available in programming languages so you need to keep them separate since they are are inherently separate.

That's an action (p21) TLA+ also defines a state function

How could someone, trying to figure out the best way to mutate state, ignore so openly the theory behind it? Especially when that theory is perfectly aligned (and never breaks) Redux's principle #1 (a single state tree) or when it defines actions as pure functions?

Do you guys really think you can beat TLA+? and find a better theory describing how you mutate state? Really?

markerikson commented 8 years ago

@jdubray : yes, yes. We get that there's a theory, written by a guy with a PhD, and that there's three-letter-acronyms that are magically supposed to solve everything that's wrong with Redux. You've made that quite clear.

To genuinely answer your first question: you seem to be the only one who has taken meaningful note of Dr. Lamport's book and theory, or at least are the only one who has tried to publicize it. Therefore, it is very easy to understand that, y'know, just maybe that work wasn't intentionally ignored, it's that no one involved in initially developing Redux had heard of it. Most of Redux's development process happened here, on Github, and the discussion happened in the issues. You can go back and see how most of the various concepts evolved into their current form.

As for everything else: it is indeed entirely possible that TLA+, SAM, and NAPs are truly superior, and the answer to all the current discussion regarding Redux, side effects, and state management. But frankly, your approach to trying to evangelize these ideas is having exactly the opposite effect.

You published an article describing some of your problems with MVC: sure. You published an article describing your alternative: good. You published a repo containing your library and some of your ideas: fantastic. Sharing information is good, sharing code is even better.

However:

Even if you did manage to explain, in clear, simple, understandable terms, exactly what the differences are between Redux and SAM, and convince me that SAM is the superior solution solely on a technical basis... I would refuse to use it, because I would not want to associate myself with something that comes from an attitude of tearing others down.

The React/Redux community is friendly. This community is positive. This community writes tutorials, shares information, answers questions, helps others. This community is coming up with nifty new ideas. This community is busy writing real applications, with real code, in real-world situations. And, it would appear that there's a very large number of developers who happen to find these tools useful.

So no, Redux isn't perfect. There's still some pain points and some concepts that people are trying to work out. And if you have some genuinely useful contributions to make (like a pull request, or a specific way that Redux could actually be improved), please do submit those so we can discuss them. But coming in here, insulting everybody, insisting we're doing it the wrong way, and that you are the only one with the right answer, is _not_ a way to win friends and influence people.

jdubray commented 8 years ago

I am not the only one talking about TLA+, if facts are what you are looking for, you may check this article: http://research.microsoft.com/en-us/um/people/lamport/tla/formal-methods-amazon.pdf

I know of other people who use it too.

It is true that even Microsoft ignored TLA+ for the last 10 years (how could Ballmer pay any interest to that kind of work?), but if there are any Microsoft employees in this thread, perhaps they can share some recent steps Nadella, himself, took towards using his work profusely at Microsoft. Actually I would not be surprised if at some point Mark (Z) has sooner or later a Satia moment.

Heck, TLA+ is even on ThoughtWorks radar.

I don't "win" or "influence" people. This is not a beauty pageant, if you think it is, you are in the wrong forum. I don't belong to a herd or looking to build one. After 36 years writing code, my experience with the herds in our industry (and the Redux one does not seem to be any different) is that as soon as you bring up some facts such as "Redux seems to be breaking his principle #1 pretty easily", or "you know, that's not really how SQL works" everyone stops talking to you. That's exactly what Yassine did as soon as I made that point. He stopped talking within a nanosecond and Dan followed. If you want to talk about "attitude" perhaps you could also comment on that kind of "attitude".

I'll stand by my words, Redux is a piece of garbage, this very thread reflects precisely why, otherwise it would not be necessary to open it, it would already be covered by Redux.

Phoenixmatrix commented 8 years ago

Last person I know who mentioned Redux being inferior wrote a pretty good blog post and has a comprehensive implementation of his ideas to go with it.

http://staltz.com/why-react-redux-is-an-inferior-paradigm.html

I don't agree with him, but it only takes a few minutes to look at Cycle.js and see the pros and cons and understand the whole point.

Elm itself is another good example of someone with an idea and doing a pretty good job at explaining it (if I remember well, Elm is the result of an academia thesis, too).

It would be great to have something like that. PhDs write thesis and papers every day. It's not super useful without a comprehensive proof of concept.

adamyonk commented 8 years ago

Is just like to chime in and say that the community around redux seems very welcoming and positive. Perhaps there hasn't been much discussion around this because it has, in the past, quickly devolved into negativity and name-calling. I just want to encourage everyone to push hard against the tendency of the Internet, which seems to always be insulting or attacking an imagined, faceless, enemy who is in fact a person dealing with their very own problems. Be encouraging, hold your temper, have a lot of patience, and great conversation will come.

jtadmor commented 8 years ago

TL;DR: +1 for a redux-effects-saga + EffectsStore that listens to all / some actions, handles effects, and can dispatch actions to Redux store.

It seems like some of the solutions here would like to see redux (or a super-redux future lib) take what redux currently does and expand it to encompass effects.

I'd be much more in favor of an approach, like many have ( e.g. #1528 ) mentioned, of viewing current vanilla redux as perhaps just one composable piece of a larger state/effect management system.

Redux is pretty good at what it does. There are definitely room for improvements and optimizations, but it's amazing to see all the awesome libraries (authored by many of the commentators here) that can be used with it precisely because it only does one thing, does it well, and does it in a composable way.

Redux doesn't manage effects. I really like the idea of keeping it that way, and instead implementing a design pattern in which there are more than one type of action / message, and at least one more message handler / store (EffectsStore) that potentially stands before redux. Because reducers are pure, they would not want to emit messages back to our EffectsStore, but EffectsStore could definitely handle some actions / messages and dispatch to redux.

Right now we are limited by not allowing components to know about effects / messages, only about updated state.

For example, the use case initially mentioned by Sebmarkage for Effects had to do with layout. It would be cool if I could emit a message when I try to resize one component. EffectsStore / EffectsSaga would receive this message, dispatch the message to other components telling them to measure their DOM attributes, do some computations, and only then dispatch to Redux store. The Redux store does not, strictly speaking, need to know about any of this until it's time to update UI-relevant state.

Redux-saga solves a lot of issues, but it currently only handles actions that will be handled by redux store, which makes it unnecessarily wasteful if you just want to pass a message from a component, where that message isn't going to immediately change application state, but is instead meant to trigger other messages / effects that will (perhaps) eventually change application state. We don't even want to run those through our reducers.

jtadmor commented 8 years ago

For similar reasons, I'm not necessarily a huge fan of storing what we might call "demand state" in Redux

{ loadingUsers: true } seems like a fine piece of data, because the UI depends on that to display a spinny wheel.

I would argue that UI should be completely agnostic to { computedQuery: { MY_WHOLE_APP_QUERY } } or anything similar. To the extent that UI is a pure function of redux state, redux state probably shouldn't include irrelevant data.

Application state, on the other hand, could be something like: { redux: {}, effects-in-progress: {} }.

But, again, why go through the hassle of updating redux, pinging every reducer, and, if we update redux state with demand that don't affect our rendering, also ask every single connect-ed component to run its "should I update" checks. Even if they are implemented well, and again many folks have helped a ton with awesome libraries, we are still running thousands of pointless computations in the best case, and triggering unneeded re-renders in many cases.

jdubray commented 8 years ago

@jtadmor why would you think that there could be "'effects" without a single change to the application state (assuming the application state is in a single state tree)? why would you think that when you change the application state, there is a (general) way to know which component will be affected? (if we update redux state with demand that don't affect our rendering,) why would you think that application is always "visible" to the user? ("redux state probably shouldn't include irrelevant data.")

On the other hand when you separate "proposers" from "acceptors" things like "where that message isn't going to immediately change application state" become trivial to implement, lots and lots of stuff can happen to decide what to propose to the application state. When users changes their mind, I can even cancel an action that has not yet presented its data to the model. I can even trigger multiple actions and let the first one to present to the model win.

When you separate "acceptors" from "learners" then the learners can take a lot of smart decision as to who needs to render or now.

But, what do I know?

jtadmor commented 8 years ago

jdubray,

I'll give a try at answering what (I think) you're asking:

_1.why would you think that there could be "'effects" without a single change to the application state (assuming the application state is in a single state tree)? _

I can imagine a design pattern in which effects change application state. This would be the { redux: {}, effects-in-progress: {} } I mentioned above. I could also conceive of a design pattern in which we don't record effects at all. The former has some advantages, but also takes work to implement. Up the developer, IMO.

2. why would you think that when you change the application state, there is a (general) way to know which component will be affected? (if we update redux state with demand that don't affect our rendering,)

There are many heuristics that help me know when certain actions will affect certain components. Developers write the app. So they can know whether the current application query state affects the visual output of the app. When I know that a certain part of state is NEVER going to affect my components directly, then I shouldn't be notifying my components when that state changes.

Also, we could easily imagine a pattern in which components register their dependencies, with the Store, in the form shouldUpdate = ({ action, currentState }) => true / false, and components would only be notified when that is true, instead of the current model where each component maintains its own shouldUpdate = ({ myProps, prevState, nextState }) => true / false type logic. But that's another conversation entirely.

Frankly, I think everything else you asked or pointed out are things that are irrelevant to what I said or things that redux already does. I'd echo what others have said: When you stop using jargon that you haven't clearly defined (without asking me to read an entire book) and take the time to charitably read other's comments, you might find your conversations more rewarding.

ccorcos commented 8 years ago

I'm not necessarily a huge fan of storing what we might call "demand state" in Redux

@jtadmor I would say that these transient states are very relevant. (1) you can display loaders as you already said, but (2) it allows you to recover from from transient async side-effects in a time-travelling debugger -- something I discovered building elmish.

jdubray commented 8 years ago

Let me explain what I mean by proposer and acceptor with a simple example. When you say counter += 1 ;

There is another way to look at it: const v = counter + 1 ; ... counter = v

It would of course extremely painful if all our code was written that way, but this is how "state" is mutated, one proposes value which are accepted. It's very healthy from a factoring perspective because you can have action specific logic (that "initCountDown" action only proposes values > 3) different from model specific logic (model accepts values >=0). Not to mention what I said before that you can manage proposals in a very smart way (cancellations, races, voting,...).

I'll refrain from commenting further on the reducer structure as I would start to be too negative, but if the Redux community could learn just one thing it would be that there are many advantages to separating proposers from acceptors. The role of an action is to propose values (in the SQL sense, as in I think the value of x is 3) to the acceptor. Acceptors have full knowledge of the application state to decide whether they want to accept this value or not, while Proposers shouldn't, Actions should never request model.getState(). What Redux calls an "action" is more an "intent". When you trap the proposers in the purity of the Reducer you create unnatural constraints that you have to somehow compensate for somewhere else.

Personally, I find these factoring debates fascinating because you can clearly see how tiny, nearly imperceptible, decisions have such an impact on the structure of your code.

jdubray commented 8 years ago

@ccorcos

I would say that these transient states are very relevant.

hum...you would not consider introducing a State function in Redux? considering that Sagas are probably the worst way possible to manage that kind of state?

markerikson commented 8 years ago

Y'know, part of the problem here is both sides are using the same terms with slightly different meanings...

jdubray commented 8 years ago

Yes, I agree that's a very big issue. There are only a handful of nouns and too many variants to make it possible to label them properly. That's also why I tend to like the Paxos labels: proposers, acceptors, learners because these are roles (hence code factoring) rather than things like actions/state,...

slorber commented 8 years ago

@jtadmor

I think the problem is that Redux sells its store as being the "source of truth", while the source of truth is the event log. The state computed by Redux root reducer is just one possible projection of that log, but you can produce as many as you want, so the "state", is not really the source of truth, but it is probably simpler for beginners to think so. For simplicity, Redux somehow combines both receiving events, and projecting them. In backend systems doing EventSourcing/CQRS these 2 parts are often separated but it requires more infrastructure (like Kafka receiving events, and Spark/Storm/Samza/whatever projecting them. Often there is not a single projection logic).

What I want to say is that you should consider indeed every dispatch as a state change, even if the reducers/connected components are not interested by this change (and thus the store's state remains the same), it's still a state change.

But I understand your concern where you want to avoid unnecessary computations when possible. In case you want to dispatch an action that will only target a saga, you can use redux-ignore to avoid hitting reducers uselessly. In case your store state needs to change but it does not affect UI and you don't want connected components to run for nothing, then it's not really a Redux problem but more related to React-redux. I'm proposing here and there more flexibility in React-redux so that we can build custom connect functions that will be more optimized unlike the default one that kicks in at every state change (even if it's fast).

These solutions are just workarounds related to the fact that Redux is both sides, while in more complex cases it's worth considering to split (requires more work). For example, instead of dispatching to Redux directly, you could dispatch to your own event log that would produce different event streams to be consumed by multiple redux stores, one of them being in charge to manage the view state (so the stream can be filtered to only contain UI-relevant events).

jtadmor commented 8 years ago

@slorber

You are correct that some of what I'm proposing bleeds into react-redux optimizations. And I agree that a kafka-esque (hehe) architecture is probably ideal anyway.

Thanks, redux-ignore looks helpful and I'll read over those proposals (like I said, I think there's a lot of room for optimization, but it's also awesome that redux does one thing really well and allows for an ecosystem of helpful tools that play nice).

But part of what I'm saying is that instead of talking about turning redux into effectively a (action) => { state, effects }, we should let it do what it do, and build other abstractions / libraries that allow for effects, rather than ruining a very nice pure one.

jdubray commented 8 years ago

(action) => { state, effects } is exactly what a controller do in MVC, you should stay as far as you can from that kind of factoring.

jdubray commented 8 years ago

@slorber The problem with Front-End architectures is that the View tends to be too coupled to the Model. There is a natural progression from user event (click) to intent (submit form) to action (proposed changes to the application state). There are smart/better ways to implement acceptors and you are describing a very important architecture for achieving just that, but you cannot ignore the fact that before you reach that stage, you need to go through an event/intent/action (aka propose) stage. The Acceptor event log is unaware of the View event/intent/action stage. It is the output of the action that makes it to the "event log".

On the other side (M->V) you need as much decoupling, which in SAM is achieved by the S() function. In other words <V props={state}/> is not enough.