In my last two posts I wrote about Higher Order Components and Context in React and how they are used to make code more maintainable. In this post I will show you an example application, that incorporates these patterns.
Consider this simple list application:
function List(props) {
return (
<ul>
{props.items.map((item, key) =>
<li key={key}>{item}</li>
)}
</ul>
)
}
function Application() {
return (
<div>
<h1>Application</h1>
<List items={['Mercury', 'Venus', 'Earth']}/>
</div>
)
}
It has a hard coded data array of items it displays.
Now the idea is, to fill the list with items from a server. This means, at some place we need to get the data and put it inside the List
.
We could do this in Application
, but this component could be the container of many other components which need other data. This wouldn't scale well.
So the naive approach is, to get the data as close as possible to the place where it's needed, in our example List
.
A rewrite of the List
component could look like that:
// This is now a stateful component
// it handles all its data retrieval
class List extends React.Component {
// we set an empty array for our items
// this can be used till the server data arrives
state = {items: []}
// after the component is in the DOM we load the server data
// the URL is in the prop "src"
// we reformat it a bit and store it in the components state
componentDidMount() {
fetch(this.props.src)
.then(r => r.json())
.then(tasks => this.setState({
items: tasks.map(t => t.title),
}))
}
// for every render we just map over the items
// and create an <li> for every one (0 in the first run)
render() {
return (
<ul>
{this.state.items.map((item, key) =>
<li key={key}>{item}</li>
)}
</ul>
)
}
}
// the application doesn't need to supply items here
// instead a source URL is needed
function Application() {
return (
<div>
<h1>Application</h1>
<List src='http://jsonplaceholder.typicode.com/todos'/>
</div>
)
}
This works rather well for simple components, it decouples the data retrieval from the container object and makes the usage of the List
more flexible. It could even be used in different places with different source URLs.
But now the visual part of the List
is tightly coupled with the data retrieval, which makes it much harder to test, if the created DOM elements are right. You always need a server or at least a mock-server that gets you the right data.
Also, it could that you want to render different server responses in the same List
component later.
One solution to this is the teaming of HOCs with context, like mentioned in the last two posts.
First you create a Service Provider Component, that handles the data retrieval and injects the results into the context.
Second you create a Higher Order Component that will gather the right data from the context and injects it into the props of its child. Also, it will trigger the retrieval.
Lets to the first task, the service provider:
class ItemProvider extends React.Component {
// in this simple example all items get written in the same place
// in a more complex system, you could make this configurable too.
state = {items: []}
// this method gets data from a source URL
getItems(src) {
fetch(src)
.then(r => r.json())
.then(items => this.setState({items}))
}
// this method provides components, who are potentially deep in the app tree
// with two things, a list of items and a function to load these items
// here we use the method getItems to load the data
getChildContext() {
return {
items: this.state.items,
getItems: this.getItems.bind(this),
}
}
render() {return this.props.children}
}
ItemProvider.childContextTypes = {
items: React.PropTypes.array,
getItems: React.PropTypes.func,
}
The second task is the higher order component:
// The first function takes configuration
// in which prop the items should be inserted
// where the items should be loaded from
// and a function that formats each item to the
// expected format of the wrapped component
// this returns a second function that takes a component to wrap
const connectItems = (targetProp, src, format) => Comp => {
// if the component that should be wrapped is supplied, we create a HOC
class ItemComponent extends React.Component {
// when its mounted to the DOM, it will use the getItems function
// supplied by the provider somewhere at the top of the component tree
// and tell it where the data should be loaded from
componentDidMount() {
this.context.getItems(src)
}
// when the component renders it simply renders the wrapped component
render() {
// the props of the HOC will be passed down to the wrapped component
// this allows to apply styling and such
// and the items from the provider will be formatted
// and stored in the target prop of the wrapped component
const newProps = {
...this.props,
[targetProp]: this.context.items.map(format),
}
return <Comp {...newProps}/>
}
}
// the HOC needs to tell React, that it needs 2 context variables
// the getItems function to start the data retrieval
// the items array to be passed down to the wrapped component
ItemComponent.contextTypes = {
items: React.PropTypes.array,
getItems: React.PropTypes.func,
}
return ItemComponent
}
Now we have a provider wo is responsible for the data retrieval and a HOC that will tell the provider when to load the data. It also passes it to its wrapped component into the right prop.
In the end we just need to pull everything together:
// List component can stay as it is
// so it's easier to test and easier to reuse
function List(props) {
return (
<ul>
{props.items.map((item, key) =>
<li key={key}>{item}</li>
)}
</ul>
)
}
// Here we wrap the List with our HOC
// first argument is the target prop of List for the items
// second argument is the target URL the items should be fetched from
// third argument is the format function that tells
// which part of an item should be used as text
// at last we add the component we want to wrap.
const ConnectedList = connectItems(
'items',
'http://jsonplaceholder.typicode.com/todos',
item => item.title
)(List)
class Application extends React.Component {
// First we wrap the whole Application with our provider
// then, somewhere deeper in the element tree, we add the Connected list
render() {
return (
<ItemProvider>
<div>
<h1>Application</h1>
<ConnectedList/>
</div>
</ItemProvider>
)
}
}
Finally, if our service changes, we just have to change the ItemProvider
and if it keeps its API (context.getItems(src)
and context.items
) it's completely transparent to the rest of the application.