RFC: Reuse complex components implemented in React plus Redux
sompylasar opened this issue ยท 43 comments
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:
- can be instantiated more than once, maybe simultaneously (not a singleton)
- each instance can have its own configuration
- can query and manipulate the global environment:
- the URL and the history (routing, back-forward)
- network communication (AJAX, WebSockets etc.)
- storage (cookie, localStorage, sessionStorage etc.)
- viewport dimensions, global events like viewport scrolling/resizing
- can depend on the app state:
- query and manipulate other components
- delegate some functionality, e.g. asset loading, full-screen modal container etc.
- should not pollute the environment
- when used from another app, the component should be reused, not copy-pasted
An app -- a UI environment where the components are configured and instantiated.
Traits of an app to consider:
- can be a React + Redux app
- can be a React-only app
- can be a non-React app
Examples of components
- a wizard, a multi-step form, a questionnaire
- a complex stateful popup, like a multi-tab settings dialog, or a chat
- a WYSIWYG editor with autocompletion and image uploads
Developing such components with Redux adds the invaluable benefits of predictability and replayability.
Questions to answer
- How to structure the component code (where to put reducers, actions, UI code)
- How to put a component into a React + Redux app
- How to put a component into an app that has no Redux and/or React
- How to isolate the state of the component instance
- How to configure the component reducers' logic based on the component instance configuration
- How to target actions at specific component instances' state
- How to handle actions of a specific component instance in the app reducers
- How to bridge the component with the global environment (URL and history, network, storage)
- How to bridge the component with the app state
- How to bridge the component with the functionality provided by an app (asset loading, full-screen modal container etc.)
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
- Similar discussion, but not so broad: #123
- Similar discussion, but about just actions / reducers / side-effects: reduxjs/redux#1171
- Similar question: reduxjs/redux#1314
- Related: redux-saga/redux-saga#62 Though I'd like to avoid sticking to redux-saga until these sagas' state is serializable as well as redux state to at least survive a page reload. ( redux-saga/redux-saga#22, redux-saga/redux-saga#5 )
- Related: reduxjs/redux#1098
- Related, about action types convention: reduxjs/redux#786
- Related: reduxjs/redux#913
- Related: reduxjs/redux#943 (comment)
- Related, reducer logic customization: BurntCaramel/flambeau reducer props
- https://github.com/erikras/redux-form
- https://github.com/tonyhb/redux-ui
- https://github.com/erikras/multireducer
- https://github.com/acdlite/reduce-reducers
- https://github.com/artsy/react-redux-controller
- https://github.com/Wildhoney/ReduxLocal
- https://github.com/gcazaciuc/redux-fractal
- https://github.com/tomchentw-deprecated/redux-component
- https://github.com/wcjohnson/redux-components
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
@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.
@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.
@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? evancz/elm-architecture-tutorial#50
By the way, I'd appreciate if you wanted to contribute to this TodoMVC-Onboarding with an Elm architecture solution.
@sompylasar maybe the DDD part of my anwser here can interest you: reduxjs/redux#1315 (comment)
@slorber ๐
@sompylasar Thanks for the kind words in reduxjs/redux#419 (comment). 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. :)
@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.
In our applications we solved problem of isolating component's logic in a connect-like style. https://github.com/Babo-Ltd/redux-state
More ideas here: reduxjs/redux#1385 (comment)
Relevant new discussion: reduxjs/redux#1528
Encounter just the same problems, and haven't found any practical solution yet. I will keep my eye on it.
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:
- its less performant (unless you're really clever about it) because every time you map over dispatch (i.e.
action => dispatch({type: 'childAction', action})
) you create a new function references which forces the component to be re-rendered on every state change. - with better encapsulation comes a tradeoff with the amount of plumbing you have to do to communicate between components (much like React with local state).
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
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.
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).
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.
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.
@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?
@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.
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 ReactiveX/RxJava#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.
@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.
@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.
@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)
Excited to see more people take serious stabs at this, as it is still one of the major unsolved problems in the Redux ecosystem ๐๐
I'm thinking autogenerated lenses would be useful.
@dalgard ok head exploding. Can you describe some use cases for lenses? It's the first I've ever heard of the concept.
I think I'd better leave that to the internet, I'm still learning all the functional stuff myself.
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.
@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.
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!
@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.
@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).
@mpeyper Sounds like what I'm talking about ๐ Looking forward to seeing it in public.
@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.
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.
@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.
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.
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:
- When hoc is mounted, it will start Redux-Saga task, initializing reducer of component, and register node on state.
- When hoc is unmounted, it will cancel Redux-Saga task, destory reducer of component, and delete node on state.
- Reducer of component will only accept actions dispatched by current component by default. Revert reducer to accept all actions by set options.
- 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.
- Like one-way data flow of Flux, child hoc could get state and actions of parent by vReducerKey.
- Integration deeply with Redux-Saga, accept actions dispatched by current component and set state of current component is more easily.
@hapood isn't it a bit similar to https://github.com/threepointone/redux-react-local?
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).
where to put reducers, actions
Use The Elm Architecture