This article proposes a solution for dynamically changing Redux containers on the fly to reuse, not only the UI components, but the Data-layer too, by switching the mapped action-creators on runtime, using two main concepts, lazy-loading (with React.lazy) and Duck Typing. If you don’t have much time to go through the step-by-step, check the resume at the end of this article.
Note I: Before copying the gists from this article, I created a repo that does the exact same thing: react-redux-lazy. Go there and check it out 😉
Note II: For the sake of simplicity, I’m using thunks, but it should work with sagas and epics too. What is important are the action-creators contract.
Imagine an app that you can manage blog posts, where the user first, login, then the app lists all the posts on the history, and finally, you can create new ones, edit or delete. Yep, the good’ol CRUD.
But knowing that the user wants your app to support as many blogs services it can, but with the basic features of any blog platform. How can you do that in a way that you can reuse your data-layer + UI? Basically you want to load posts, edit them (or create new ones) and send to the server, right?
But hey, you don’t want to bloat your app with dozen of business logic and SDKs for all the supported platforms at once! Just load the code for that platform if the user choose that platform.
Although we’re approaching this CRUD app, this solution can be applied to many scenarios that you have a certain generic scope, where the actions and the data are similar.
If you think in an app that uses React and Redux, being simplistic, you can divide it in this 3 parts:
- React Components -> User interface
- Store (Reducers, App state) and Actions -> Data layer
- Action creators (thunks, sagas, epics) -> Business Rules, Side Effects, Data processing
And there’s one entity responsible to tie this together, they are the Containers. They’re non visual Components that makes a bridge between the store, more specifically action-creators (wrapped to dispatch) and the App State, to your UI components.
As you can see, is there where we’re going to introduce the lazy loading, using the strategy pattern to load the proper business rules on the fly.
But hey, how the UI Components and the Reducers are going to accept different action creators? This is the trick, you’ll use Duck Typing, and build up action creators that dispatch the same action in the end, but internally they’re doing that through a different way (of course, each platform have it’s own SDKs and specificities)
For instance, thinking in our blog manager example, we’re going to have the PostEditor component. Let’s suppose this component will need the following actions:
- updatePost(postId, postText)
- loadPost (postId)
For each and every platform, we’re exposing this very same actions-creators, internally using their own flows, but always dispatching the same actions, with same “type” and “payload” data format. This is important for the reducers, as they’re expecting data on a defined format (a contract) from the actions.
Let’s see what we want to achieve:
The state tree is going to be the same, so we can define the mapStateToProps as normally is done. The dynamic thing here are the action-creators, so the mapDispatchToProps are going to be async.
But as you may know, the connect from “react-redux” accept objects for the mapDispatchToProps, so we’ll need to create a “lazy” version of the connect.
With our lazyConnect implemented, let’s check how we can use it:
So, the LazyPostEditor is the dynamically connected version of PostEditor, and will pass forward all props passed to it.
Of course, you can let the responsibility to wrap on <Suspense/> for the component which will use it, and also, don’t have any visual elements on the container (like the loading indicator), you can achieve this by returning directly the “lazyConnect” result. But to maintain this example simple, let’s keep it like this. So, you can simply use this as a common component:
This will make the actions inside “actions/blogo-blog/post.js” be bound to the PostEditor component through the props, and every time this component calls one action, is going to be for the right platform.
But let’s see how are the actions of “blogo-blog” and another fictional platform, “blog-fu”:
As you can see, both expose the “same” methods. Although you can expose these functions with different names/nicknames (yes you can put different names on them as long you map them properly to the Component using the mapDispatchToProps) you need to follow the same argument signature, as you’re expecting a call pattern coming from the component (remember, Duck Typing!).
Other important aspect is the data format on the action payload. As you can see, both SDKs deliver them in a different format, but as the reducers are expecting it in a protocoled format, you’ll need to parse it. The downside on this is that you may end losing information, as all platforms have their specificities, but hey, this is a common side-effect of adding an abstraction layer to make something specific, generic.
Ok, let’s wrap up.
- Define a generic state tree (+reducers, +actions) with your solution in mind. In this article, the example is for blogs, in your case can be like loading users from different providers, or locations of restaurants, doesn’t matter, just make sure to create a solid entity that can attend the core information that exists on all data sources;
- Create an UI that reflects your app state (resuming, do what you normally does on a react+redux app);
- Define the action-creators, for all the data-sources, following a common signature (params and dispatched action) to attend your reducers;
- Instead of using the react-redux “connect”, use the “lazyConnect” to bind the right action-creators (dynamically imported based on params) to your React Component (wrapping in a lazy Container for that);
- Add a thousand of data-sources/platforms and be happy!
Reusing the UI + Data Layer for a generic solution will make you lose specific data/behaviour, but this is a common side effect of any abstraction layer you add to your application, so if you really need a generic solution this will not be a big contra. In the other side, the pros of such approach will make your app easy to extend. Is like a plug and play solution for data: add more platforms by simply following the protocol of the actions and the path/filename and you’re pretty much done (check open-closed pattern from SOLID).
Other important aspect are the tests. As you’re using a strategy pattern to select the action creators, you can add a fictional platform on your test runs without the need of proxying the imports or dependency injection, you’ll just get this “feature” out of the box!
Such abstractions as the lazy/Suspense, added to React, are just a ladder for us, developers, to think outside the box and create awesome solutions from that.