winstonewert / redux-reactions

A reactions approach to side-effects in redux.
MIT License
6 stars 2 forks source link

Routing reaction events #7

Open kurtharriger opened 8 years ago

kurtharriger commented 8 years ago

I was trying this pattern a bit in the react-elmish-example as I was hoping lifting the side-effects out of the action creators would enable better composibility of components without awareness of there context. However it still seems to have the same problem since reaction events are essentially action creators and the dispatch function is provided by the reactions middleware not by the parent component so it can't be wrapped.

The redux counter example has an incrementAsync action, but only one counter, and the elmish example has a list of counters but no asynchronous or impure side-effects. One modifications to make this support multiple counters do so by making the container aware explicitly aware of its identity in the application which limits composability. Another alternative is to have the parent component wrap the dispatch method but makes traditional async middleware unusable. See discussion on https://github.com/rackt/redux/issues/822

In this specific real-world example the state isn't component specific at all really so there doesn't seem any need to route the response back to one of many places in state with similar information, but if you take the react-elmish-example and try to add incrementAsync or add more than one counter to the current counter example you'll find that you need a way to figure out which counter needs to handle the reaction. It doesn't seem sufficient in this case to simply wrap the reaction response since the event as a promise is opaque to the parent component.

One option might be to pass the dispatch function to reactions, not to be called by reactions directly, but to be provided to the reaction creator to use when dispatching the resulting event so that it can be routed. This seems simplest but also possibly easiest to misuse as dispatch should not be done from the reactions function but only in doReactions, but doReactions needs to dispatch events possibly using a dispatch method that has been replaced by a parent component to route the event to desired child component.

winstonewert commented 8 years ago

It seems to me that you could wrap the action creators in the events to attach additional context information:

_.map(reactions, (reaction) => ({
    ...reaction, 
    events: _.mapValues(reaction.events, (actionCreator) => 
         (...args) => ({
             ...actionCreator(...args),
             counterIndex 
         })
      )
  })
winstonewert commented 8 years ago

On the other hand, this would require both wrapping dispatch and remapping the action creators. Perhaps for consistency with react's model, the library should really just call the events functions, and let them take care of dispatching.

winstonewert commented 8 years ago

I re-read the elm documentation, and the light bulb went off in a way it hadn't before.

You may find my reworking of the counter example interesting: https://github.com/winstonewert/redux-reactions/tree/elmish/examples/counter

kurtharriger commented 8 years ago

Yes, this is almost exactly what I was thinking. I haven't had a chance to look too deep at it yet, but I think you understand the problem. The dispatch function has the role of the mailbox address in elm. In elm the ability to forward and map events to another mailbox is key to being able to compose components without them being aware of their context. Additionally, whatever is done in the dispatch function to wrap effects needs to be unwrapped in the reducers.

It feels a bit complex to me though still, but I think this is mostly organizational though. While this follows the recommended redux layout I don't think it's the best layout for reusable components as the views, reducers, actions, reactions are generally coupled around a state shape. Elm generally organizes all these into the same module which I think is more cohesive and easier to reason about.

But yeah I think this shows you can isolate and compose redux components/containers if and perhaps only if action creators stay simple and you lift side effects into reactions rather than placing them in the action creators.

winstonewert commented 8 years ago

I did another iteration, which I think improves it.

The elmish pattern is very nice and composable, but the contrast between the redux and elmish ways are becoming more apparent. Its leading me to wonder if it makes sense to continue using redux with this pattern at all.

kurtharriger commented 8 years ago

Yeah. This structure makes it much easier to see what goes with what and introduce new behavior in one place rather than several.

I suppose one advantage of not using redux is that users are more likely to take a fresh look at where they should place asynchronous behavior and side effects and you could pretty much gut support for middleware or rethink it in a way that is more compatible with reuse and context isolation.

But I think redux community seems to be somewhat aware that reusing components is not quite a solved problem yet and maybe some documentation and/or blog post highlighting these differences and bumping the redux issue thread again might surface more reviewers and opinions than just mine. It would definitely be easier for me to sell my organization on a major version bump to redux than switching libraries yet again.

Maybe there is some sort of middle place where this pattern might be used for reusable redux components like redux-form until the desire for everything to compose as easily slowly takes over the rest of the code base and usage patterns and best practices change.

If nothing else I've learned a lot from seeing this evolve and it has changed the way I think about structuring redux applications, so thank you!