Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
sompylasar opened this issue Feb 2, 2016 · 43 comments
Closed

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

sompylasar opened this issue Feb 2, 2016 · 43 comments

Comments

@sompylasar
Copy link

sompylasar commented Feb 2, 2016

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

@tomkis
Copy link

tomkis commented Feb 3, 2016

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
Copy link
Author

@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
Copy link

tomkis commented Feb 3, 2016

@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
Copy link
Contributor

slorber commented Feb 6, 2016

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

@slorber
Copy link
Contributor

slorber commented Feb 6, 2016

@sompylasar maybe the DDD part of my anwser here can interest you: reduxjs/redux#1315 (comment)

@sompylasar
Copy link
Author

@slorber 👍

@ghost
Copy link

ghost commented Feb 14, 2016

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

@sompylasar
Copy link
Author

@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
Copy link

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

@sompylasar
Copy link
Author

More ideas here: reduxjs/redux#1385 (comment)

@gaearon
Copy link
Contributor

gaearon commented Mar 18, 2016

Relevant new discussion: reduxjs/redux#1528

@panezhang
Copy link

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

@sompylasar
Copy link
Author

@ccorcos
Copy link

ccorcos commented Oct 20, 2016

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

@tiengtinh
Copy link

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
Copy link
Contributor

mpeyper commented Jun 22, 2017

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
Copy link
Member

timdorr commented Jun 22, 2017

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
Copy link
Contributor

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
Copy link

@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
Copy link
Contributor

@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
Copy link

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.

@mpeyper
Copy link
Contributor

mpeyper commented Jul 5, 2017

@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
Copy link

@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
Copy link
Contributor

mpeyper commented Jul 5, 2017

@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
Copy link

dalgard commented Jul 5, 2017

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
Copy link

dalgard commented Jul 6, 2017

I'm thinking autogenerated lenses would be useful.

@jcheroske
Copy link

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

@dalgard
Copy link

dalgard commented Jul 6, 2017

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
Copy link
Contributor

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
Copy link

dalgard commented Jul 7, 2017

@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
Copy link

dalgard commented Jul 7, 2017

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
Copy link
Contributor

@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
Copy link
Contributor

mpeyper commented Jul 8, 2017

@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
Copy link

dalgard commented Jul 8, 2017

@mpeyper Sounds like what I'm talking about 👍 Looking forward to seeing it in public.

@dalgard
Copy link

dalgard commented Jul 8, 2017

@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
Copy link
Contributor

azizhk commented Jul 27, 2017

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
Copy link
Author

@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
Copy link
Author

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
Copy link

hapood commented Sep 10, 2017

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
Copy link
Contributor

slorber commented Sep 11, 2017

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

@hapood
Copy link

hapood commented Sep 11, 2017

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

@mpeyper
Copy link
Contributor

mpeyper commented Sep 21, 2017

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
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests