reduxjs / react-redux

Official React bindings for Redux
https://react-redux.js.org
MIT License
23.35k stars 3.37k forks source link

RFC: Reuse complex components implemented in React plus Redux #278

Closed sompylasar closed 6 years ago

sompylasar commented 8 years ago

This post is meant to be a start for a discussion. Moved from here as @gaearon suggested here.

Intro

I'm interested in finding best practices on how to architect complex components implemented in React and Redux so that they are reusable as a whole in another app.

Not sure how widespread is the problem, but I encounter it from time to time. I hope the developers from the front-end community encounter similar problems, too.

Terms and definitions

A complex component -- a UI (React, Redux actions), coupled with business logic (Redux reducer), and data access logic (Redux actions' side effects; middleware).

Traits of a complex component:

An app -- a UI environment where the components are configured and instantiated.

Traits of an app to consider:

Developing such components with Redux adds the invaluable benefits of predictability and replayability.

Questions to answer

React developers from Facebook answered that I should "start by reusing React components only", but having a lot of business logic copied from app to app is not the best way to go.

Elm architecture answers some of the questions, but Redux is quite different (no view+reducer coupling, no explicit serializable side-effects).

References

tomkis commented 8 years ago

tl;dr: Use The Elm Architecture

Here's my proposal:

Not sure how widespread is the problem, but I encounter it from time to time. I hope the developers from the front-end community encounter similar problems, too.

Unless you like spaghetti code, the problem is indeed very widespread, because by default Redux does not force you to encapsulate (except combineReducers but that's not enough) and therefore componentize.

I believe The Elm Architecture has found the solution for all the problems above.

The principle behind The Elm Architecture is basically just simple composition. People who are using Redux nowdays definitely knows of composition... we are composing our views, state and even reducers

The Elm Architecture is doing same - except one small thing, it's composing Actions and Side Effects as well. Just imagine you could have something like:

{
  type: 'PARENT_ACTION',
  payload: {
    type: 'CHILD_ACTION',
    payload: 'foo'
  }
}

Fairly simple concept which solves everything.

Before reading following explanation I highly encourage you to go through The Elm Architecture description

can be instantiated more than once, maybe simultaneously (not a singleton)

Just compose actions and add ID of the instance so the hierarchy could look like Counters.Counter.1.INCREMENT where 1 stands for the index of the Counter. example in Redux

each instance can have its own configuration

Have an init action, which configures the instance (Using Action composition).

can query and manipulate the global environment:

This means to make the component capable of Side Effects... Elm solves this by reducing them in updater function (Updater is same as reducer in Redux). With Redux, there are store enhancers which supports this kind of functionality already. Please keep in mind that using Generators for side effects is opinionated and has its drawbacks, but you can always use plain old reduction as Pair<AppState,List<Effects>> which works without generators too. example in Redux

can depend on the app state:

Parent components should be responsible for orchestrating inter-component communication => therefore just simple composition, I blogged about this.

should not pollute the environment

Every Component in The Elm Architecture is independent and isolated, there's no way to access parent's component state in the child component.

when used from another app, the component should be reused, not copy-pasted

And because Components are isolated, it's fairly simple to integrate it into any other redux-based application. example in Redux

sompylasar commented 8 years ago

@tomkis1 Thanks for this great overview! I'm familiar with the Elm Architecture, but the missing piece was this library https://github.com/salsita/redux-elm/ which looks like new kid on the block.

Several real-world questions aren't yet answered for me, but I'll study the examples from this repo first.

tomkis commented 8 years ago

@sompylasar Please keep in mind that it's not a framework nor library, it's just a proof of concept that we can write Elm like programs using redux. Good thing is that using this approach will solve many problems which otherwise needs some solution while using redux.

slorber commented 8 years ago

@tomkis1 I like the Elm architecture and it seems perfect to handle local component state, however I think it's missing something for real world apps.

Wrapping actions according to the dom tree structure means at the top your mailbox basically only receive some kind of global action like APP_STATE_CHANGED, and it's the deeply nested payload of that action that actually holds the useful action. So if you have an app with a lot of counters everywhere, at very different nested levels, it seems pretty hard for me to listen to ALL the increment actions of ALL counters, and display that value somewhere.

I've written something here and did not get any good answer but maybe you can try to solve my counter problem? https://github.com/evancz/elm-architecture-tutorial/issues/50

By the way, I'd appreciate if you wanted to contribute to this TodoMVC-Onboarding with an Elm architecture solution.

slorber commented 8 years ago

@sompylasar maybe the DDD part of my anwser here can interest you: https://github.com/rackt/redux/issues/1315#issuecomment-179164091

sompylasar commented 8 years ago

@slorber :+1:

ghost commented 8 years ago

@sompylasar Thanks for the kind words in https://github.com/reactjs/redux/issues/419#issuecomment-183775729. I believe everything in my article, React, Automatic Redux Providers, and Replicators, covers most of your questions and provides solutions for nearly all of them. I'd be glad to answer any specific questions. In advance, if you can include some background/reasoning behind your questions, it would help me answer them to the best of my ability. :)

sompylasar commented 8 years ago

@timbur Yes, thank you, I'm very excited with the article, that's exactly what I was looking for. I'm still reading it now, I'll ping you here if something comes into mind. One thing for now is I wonder how would redux-saga fit into the proposed architecture.

galkinrost commented 8 years ago

In our applications we solved problem of isolating component's logic in a connect-like style. https://github.com/Babo-Ltd/redux-state

sompylasar commented 8 years ago

More ideas here: https://github.com/reactjs/redux/issues/1385#issuecomment-184805927

gaearon commented 8 years ago

Relevant new discussion: https://github.com/reactjs/redux/issues/1528

panezhang commented 8 years ago

Encounter just the same problems, and haven't found any practical solution yet. I will keep my eye on it.

sompylasar commented 8 years ago

Related: https://github.com/reactjs/redux/blob/master/docs/recipes/IsolatingSubapps.md

ccorcos commented 7 years ago

I've been playing around with the Elm (0.16) architecture for a while -- there are two main issues in regards to using that architecture with Redux:

If you're interested, here's some of my latest examples of playing around with the elm pattern:

https://github.com/ccorcos/elmish/tree/narrative/src/tutorial

tiengtinh commented 7 years ago

Had a look at @sompylasar article and try out the example. It's great in term of reducing boilerplate, but I couldn't find any part solving the problem with reusing complex component. On the other hand, I stumble into this library which seems to solve the exact problem https://github.com/datadog/redux-doghouse. It's comparable to the redux-elm (now called prism) solution, I think.

mpeyper commented 7 years ago

Just stumbled across this discussion and thought I'd drop a link in to my library for solving this problem, redux-subspace. It creates a sub-store (backed by the root store) and can automatically namespace actions to isolate them from the parent components.

It has been designed so that the complex child components (which we have dubbed micro-frontends, but we have used it on really small complex components too) are completely unaware that they are in a "subspace" instead of a regular react-redux provider. The parent component decides where in it's state (which could also be a subspace for all it knows - subspaces can be nested arbitrarily deep) the child component's state is kept which make it really resilient to refactoring, multi-instance and reuse in multiple apps (we use redux-subspace for all these cases).

timdorr commented 7 years ago

Neat stuff. I was actually thinking about building a "next gen" Redux that takes the concept of composable stores to the core, letting you individually enhance and maintain them, but also link them together in interesting hierarchies.

markerikson commented 7 years ago

For what it's worth, I've collected a list of all of the "per-component state" and "encapsulated store"-type libs that I've seen in the Component State and Encapsulation section of my Redux addons catalog. And yes, that includes both redux-doghouse and redux-subspace already :)

Tim, drop by Reactiflux sometime and we can chat about that idea.

jcheroske commented 7 years ago

@markerikson (and anyone else for that matter), can you offer any words of wisdom when it comes to choosing a namespacing library? I used prism to build a google maps autocomplete component, that also used redux-observable. I don't like the implementation, and am hoping to find something better. The main thing that doesn't smell right to me is how prism monkey patches the store, so that dispatching global actions no longer works correctly. Sagas and epics also don't work as expected and require a bit of extra plumbing to get going. Do you have a favorite fractal component lib or pattern?

markerikson commented 7 years ago

@jcheroske : I haven't actually played with any of the libs in my list, just cataloged them :) I think it would be great if someone did compare a whole bunch of them and write up some thoughts on similarities, differences, and use cases, but I've got way too much other stuff on my plate to tackle that myself.

I'm a bit curious what you mean by "monkey patches the store". I skimmed the Prism source and didn't see anything obvious, unless you mean the part in prism-react where it overrides the fields in the store object passed down through context. I've seen several other libs use that approach as well. It's not a "standard" technique, but it seems like a valid approach for certain problems like this.

jcheroske commented 7 years ago

Yeah, that's what I'm talking about. It works, until you want to bust out of the sandbox. Say you wanted to call an action creator that's part of a 3rd-party lib, like react-redux-form. All of the actions dispatched will get prefixed, which is probably not what you want. So there needs to be some kind of escape hatch. Or better yet, scope the actions by using a separate scope property of the action, instead of prefixing the action type. Then you can dispatch all you want. If a reducer wants to look at the scope field and respond accordingly it can. But if it just wants to look at the action type and fire for all actions of that type it can.

On the flip side of that, there is the issue of epics and sagas. Since they are part of the fractal component, they need scoping applied. I have yet to see a lib ship with helpers that scope those. I wrote a wrapper that scopes epics, but it's ugly. Due to how the observable pipeline works, it's hard to unwrap and re-wrap actions. Essentially, it's that the observable metaphor makes it hard to pass metadata down the chain without creating a container object to hold everything. See https://github.com/ReactiveX/RxJava/issues/2931 to understand the issue. I'm about to give redux-logic a try, as it seems to be the best side-effect approach I've seen so far.

mpeyper commented 7 years ago

@jcheroske I don't want to sound too much like I'm spruiking my own library here, but redux-subspace gets around around the 3rd party issue by allowing either the component or the app specify actions as global actions.

We are also currently working on sagas-support and definitely open to looking at the observable pipeline to see if we can sort a solution for that.

We also opted for the store wrapping approach, so if that still makes you uncomfortable, then no hard feelings.

jcheroske commented 7 years ago

@mpeyper what state gets added to an action to scope it? Do you alter the type with something like a prefix, or do you add a new property? I ask because I think the latter approach may have some advantages, but I haven't actually tried it out.

mpeyper commented 7 years ago

@jcheroske, we prefix the type.

There are advantages and disadvantages to both approaches, and neither will be work in all cases, all the time.

One of my favourite advantages of the prefix approach is it makes tracing actions in the redux dev tools really easy.

In the end, the prefix approach was what we were already doing manually at my company so it made transitioning to subspace a bit smoother.

I actually have a stash somewhere where I also added a scope property to the action, but I couldn't see enough use cases to have both.

I don't want to derail this discussion too much so feel free to come have a discussion in our repo (just raise an issue with questions or comment).

p.s. I may have cracked the isolated sagas problem last night (Australian time)

dalgard commented 7 years ago

Excited to see more people take serious stabs at this, as it is still one of the major unsolved problems in the Redux ecosystem šŸ‘šŸ‘

dalgard commented 7 years ago

I'm thinking autogenerated lenses would be useful.

jcheroske commented 7 years ago

@dalgard ok head exploding. Can you describe some use cases for lenses? It's the first I've ever heard of the concept.

dalgard commented 7 years ago

I think I'd better leave that to the internet, I'm still learning all the functional stuff myself.

https://medium.com/@dtipson/functional-lenses-d1aba9e52254

markerikson commented 7 years ago

FWIW, lenses / cursors / actions with "state paths" are not exactly the encouraged approach with Redux. I linked and quoted Dan's prior comments on how they related to Redux in my post The Tao of Redux, Part 1 - Implementation and Intent.

dalgard commented 7 years ago

@markerikson I can't find Dan's comments about lenses in your post, can you help me? It is difficult for me to see why they would be in contrast to the philosophy of Redux.

Maybe if reducers were accompanied by lenses, it would be possible to compose them rather than combine them with delegation.

dalgard commented 7 years ago

Another idea would be to have each component add to a selector/lens that is passed down via context. Like redux-subspace, but perhaps more automatic.

I think this should probably be the ideal: https://github.com/staltz/cycle-onionify/

If something like that could be implemented for Redux ā€“ that is, without observable streams ā€“ that would be fantastic!

markerikson commented 7 years ago

@dalgard : Woops, my bad - Dan's comments are in Part 2, not Part 1. The specific anchor in that post is http://blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao-of-redux-part-2/#cursors-and-all-in-one-reducers .

Basically, Dan doesn't like the idea of "write cursors" because it's impossible to trace what part of the app actually triggered an update to a given portion of state.

mpeyper commented 7 years ago

@dalgard, FWIW, at my company we have another library we are using internally that uses subspace and an dynamic reducer solution to make it all a bit more automatic.

Basically, it's a HOC that injects the reducer on mounting and then wraps the WrappedComponent in a subspace using the new reducer's node as the root of it's state.

I actually wrote it a while ago, but we have only just started to use it in our apps, so after it's been road tested a bit more, we plan to open source it as well (it's looking promising).

dalgard commented 7 years ago

@mpeyper Sounds like what I'm talking about šŸ‘ Looking forward to seeing it in public.

dalgard commented 7 years ago

@markerikson From your post, it appears that Dan is discouraging the use of cursors as an alternative to reducers, which is obviously bad.

In my thinking, every reducer would get the root state (and could thus be composed rather than combined with a map) but changes it through a lens that is available to it somehow.

azizhk commented 7 years ago

What are your thoughts on just creating multiple redux stores: From https://github.com/reactjs/react-redux/blob/master/docs/api.md#examples-2

import {connect, createProvider} from 'react-redux'

const STORE_KEY = 'componentStore'

export const Provider = createProvider(STORE_KEY)

function connectExtended(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  options = {}
) {
  options.storeKey = STORE_KEY
  return connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    options
  )
}

export {connectExtended as connect}

And then just use the above connect and Provider

Should allow multiple instances of the complex component as long as they are not nested one inside another. Though I am not sure of if the different branches of component structures have different context or its just one global context object. If different branches have different context then multiple instances would be ok, but if its the same then one instance might override the store of another instance. Even in that case these can just take the STORE_KEY as argument and return the necessary connect and Provider. Should solve most of the requirements but still haven't figured how to hydrate state from SSR.

sompylasar commented 7 years ago

@azizhk Multiple stores are definitely one of the ways. I used this approach when I was gradually introducing React components as mini-apps into a large app with a proprietary component framework, but I used createStore in each of the React components to create the private stores. There are several libraries on npm that implement that approach differently, I think the References section links to them.

sompylasar commented 7 years ago

I just had an idea related to SSR (server-side rendering) and and state restoration of the multiple instances of a complex component that are plugged into the shared state tree, with a shared reducer and shared actions. Each instance needs an identifier that is included into the dispatched action objects to tell the reducer which state object to operate on. Some libraries use a user-provided identifier (the user needs to generate and provide that identifier), some generate a random unique identifier (not restorable because the identifier is generated anew on component mount). One more way to make that identifier is to calculate a hash of the serializable props of that component. This way components with the same values of the props provided from the outside (the ownProps of mapStateToProps) will connect to the same state object; components with different props will connect to different state objects.

hapood commented 7 years ago

Redux-Arena is the solution of our team.

Redux-Arena will export Reudx/Redux-Saga code with React component as a high order component for reuse:

  1. When hoc is mounted, it will start Redux-Saga task, initializing reducer of component, and register node on state.
  2. When hoc is unmounted, it will cancel Redux-Saga task, destory reducer of component, and delete node on state.
  3. Reducer of component will only accept actions dispatched by current component by default. Revert reducer to accept all actions by set options.
  4. Virtual ReducerKey: Sharing state in Redux will know the node's name of state, it will cause name conflict when reuse hoc sometime. Using vReducerKey will never cause name conflict, same vReducerKey will be replaced by child hoc.
  5. Like one-way data flow of Flux, child hoc could get state and actions of parent by vReducerKey.
  6. Integration deeply with Redux-Saga, accept actions dispatched by current component and set state of current component is more easily.
slorber commented 6 years ago

@hapood isn't it a bit similar to https://github.com/threepointone/redux-react-local?

hapood commented 6 years ago

@slorber Yes, very similar, only feature excluded in redux-arena is vReducerKey.

mpeyper commented 6 years ago

Some people here might be interested to know that redux-subspace v2 (previously mentioned) was just released and now comes with support for redux-promise, redux-saga, redux-observable and redux-loop (as well as still supporting redux-thunk).

stereobooster commented 6 years ago

where to put reducers, actions

Use The Elm Architecture