
14 Jun 2017 Extending React’s container/presentation pattern to share business logic between apps
One of the hottest JavaScript libraries out there right now is React. Whilst there are many good reasons for React being so popular, the main reason my team picked React for our new project is because of the commonality between React and React Native. The concepts and the language are the same, so if you can write React, then you can write React Native. There is also the benefit of being able put both in the same project and then share business logic between them, which is precisely what we need to be able to do.
There are a number of ambitious projects out there that aim to eliminate duplication between platforms by using a common set of React primitives. react-native-web, ReactXP and react-primitives spring to mind. However, these rely on not only a common set of components, but also a common approach to things like styling and animations.
Whilst we wait for the dust to settle on which is the best generic solution to use, my team has come up a simple approach that extends upon a pattern that is already quite well known in the React community. In this post I’ll outline this approach.
What makes sharing so easy?
In a nutshell, our approach is about sharing as much logic as possible by re-using container components between platforms, along with all of their associated business logic. If you want to know what a container component is and how it differs from other components, have a look at this article by Dan Abramov.
Our apps use Redux alongside React and find the presentation/container pattern great for separating store-related logic from presentation logic. Separating these concerns out also creates a divide between logic that can be used cross platform and the logic that is specific to a platform.
That being said, I think the container component pattern is great even without Redux because of how it separates concerns. If you have a project that doesn’t use Redux but still want to make it cross platform, container components are still a great tool for the job.
How do we share the logic?
Let’s go through a simplified version of a cross-platform app. In the interests of keeping things simple, we won’t use React Native or Redux (although we’ll come back to Redux later). Instead, this will consider two simple web apps that have the same business logic but separate presentations.
Let’s take a site that displays articles and gives the user a bit of functionality around what articles are displayed, and in what order. You’ll find the full source code here, but I’ll include some excerpts as we work through it below.
We’ll have two versions of the site, which we’ll refer to as ‘Delta’ and ‘Epsilon’. The difference between the two will be mainly markup and CSS, but one could just as easily be written with React Native.
Let’s start with the bootstrap point for the Delta app, which is stored in index.delta.js
:
import React from 'react' import ReactDOM from 'react-dom' import App from './components.delta/App' ReactDOM.render( <App />, document.getElementById('root') );
This file is referred to by our index.html
file. It loads the App
component from the components.delta
folder. and is the starting point of the Delta version of the app. It starts the ball rolling for what will be contained within, including both the common code and the code that is unique to that version.
Note that there is a matching index.epsilon.js
file that bootstraps the Epsilon app. Switching to use this version of the app is as simple as altering index.html
to refer to it instead of index.delta.js
.
Next let’s drill into the Delta App
component. It’s a presentational component that will render our first container component, ArticleListContainer
:
import ArticleListContainer from '../containers/ArticleListContainer' import ArticleList from './article/ArticleList' class App extends Component { render() { return ( <div className="App"> <div className="Title">Links</div> <ArticleListContainer component={ArticleList}/></div> ) } }
There are two important things to note in this file. Firstly, ArticleListContainer
is shared between both the Delta and Epsilon apps. However, the presentation component ArticleList
is specific to the Epsilon app.
In order to know which child component to render next (which will differ between the Delta and Epsilon apps), we pass the child component class (in this case ArticleList
) into the container component as a property.
If you look at the source code for ArticleListContainer
, you’ll see how it contains all of the app-independent state management and business logic for an article list. This includes functions for adding new articles, favouriting them, and removing them.
Then, in the ArticleListContainer.render()
method, it uses React.createElement()
to dynamically instantiate the presentation component that it has been given, and then passes business-logic related properties into it:
render() { const componentProps = { ...this.state, addNewArticle: this.addNewArticle, showNewArticleForm: this.showNewArticleForm, favouriteArticle: this.favouriteArticle, removeArticle: this.removeArticle } return React.createElement(this.props.component, componentProps) }
The container actually has no idea what this.props.component
is, it just assumes that it will accept the properties that it is passed.
The great thing about this pattern is that it scales downwards. For example, if you look at components.epsilon/article/ArticleList.js
, you’ll notice that it has its own child component for entering a new article. However, this child component has its own business logic that we want to share between the Epsilon and Delta apps. So to achieve this reuse, we take the same approach as before: encapsulate that shared logic in a container, and pass the presentation component that we want the container to use into the container as a prop:
import NewArticleFormContainer from '../../containers/NewArticleFormContainer' import NewArticleForm from './NewArticleForm' ... <NewArticleFormContainer component={NewArticleForm} addNewArticle={this.props.addNewArticle} />
So once again we have a shared container component and an app-specific presentation component. This pattern can applied all the way down through the component tree: an app-specific presentation-component embeds a shared container component, parameterising it with an app-specific presentation child component for the next level down. The app-specific presentation child component then repeats this pattern for its own children.
Is it that simple?
It sure is! By moving all of our complex shared logic into containers that can be parameterised with a presentation component, we can have separate apps – each with a different bootstrap file – that reuse those containers.
That said, if you’re using Redux – specifically, react-redux – there’s one caveat. react-redux has an important function called connect()
. This creates a factory function that you can use to automatically create container components around a presentation component. These container components will be magically wired up to your Redux store so that they can get data out of it and dispatch actions to it.
The only catch is that, in normal usage of connect()
, you specify the presentation component you want to wrap straight away. For example, if you wanted to use connect()
to automatically create a container for an ArticleList
, you’d do something like this:
... function mapStateToProps(...) { ... } function mapDispatchToProps(...) { ... } const ArticleListContainer = connect(mapStateToProps, mapDispatchToProps)(ArticleList) ...
In our case, we want to defer having to specify which presentation component is being used. Furthermore, when we do specify it, want to be able to do it via a property called component
.
Fortunately my team came up with a solution to this, which was to create a generic component whose one job is to render an unknown presentational component and pass some properties through to it. This generic component – which we call the UniversalContainer
– is passed to connect()
every time it’s used:
... const ArticleListContainer = connect(mapStateToProps, mapDispatchToProps)(UniversalContainer) ...
Then, just as before, the ArticleListContainer
can be parameterized at render-time with the presentation component to be rendered:
... <ArticleListContainer component={ArticleList}/> ...
We’ve open-sourced UniversalContainer
here if you think it might help you.
Conclusion
One of the most troublesome problems that has existed in software development has been needing to rewrite your code for different platforms. There has been a lot of work over time to try to find a solution to this, and with each iteration we’re coming closer to something that is performant, easy to write and maintain.
With this easy extension to the React container/presentation pattern, we’re able to shift our thinking, focus on creating commonality between the complex part of the app, and leave the parts that are very platform-centric separate. We can now have the best of both worlds, fine-tuning the look and feel of our apps to work best on the platform they’re meant for, while also not having to worry about rewriting any shared logic.
Pingback:TEL monthly newsletter – June 2017 – Shine Solutions Group
Posted at 17:17h, 21 July[…] Syme, whose family famously invented the letter “L” in 1826, wrote about “Extending React’s Container/Presentation Pattern To Share Business Logic Between Apps&#… – demonstrating that his technical knowledge is only surpassed by his inability to write a […]
Leo Lei
Posted at 20:08h, 11 JanuaryAwesome thinking here!
I found this writeup because I was just thinking of how to implement something like this.
I ran into multiple presentational components which share similar props but they exist just because we need to present the same data in different ways. It would be very un-DRY if we had to duplicate the container and change it to render a different presentational component.