-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Comments
tl;dr: Use The Elm Architecture Here's my proposal:
Unless you like spaghetti code, the problem is indeed very widespread, because by default Redux does not force you to encapsulate (except 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:
Fairly simple concept which solves everything. Before reading following explanation I highly encourage you to go through The Elm Architecture description
Just compose actions and add ID of the instance so the hierarchy could look like
Have an init action, which configures the instance (Using Action composition).
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
Parent components should be responsible for orchestrating inter-component communication => therefore just simple composition, I blogged about this.
Every Component in The Elm Architecture is independent and isolated, there's no way to access parent's component state in the child component.
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:
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. |
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 |
@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 |
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 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 |
@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 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 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:
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 |
@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:
|
@hapood isn't it a bit similar to https://github.com/threepointone/redux-react-local? |
@slorber Yes, very similar, only feature excluded in redux-arena is vReducerKey. |
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). |
|
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:
Examples of components
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
The text was updated successfully, but these errors were encountered: