In my last post I wrote about higher order components (HOC) as a way to decouple components from services. In this I will explain their use with the help of the context in React. This will make the last example a bit more practical.
If you used React you have probably seen a component structure like this at the root of the application:
<ServiceProviderA>
<ServiceProviderB>
<Application/>
</ServiceProviderB>
</ServiceProviderA>
And then later some component to be defined like this:
const List = props =>
<ul>
{props.items.map(i => <li>{i}</li>)}
</ul>
const ConnectedList = connectServiceA(List)
or even
const ConnectedList = connectServiceB(connectServiceA(List))
In this example the connectService functions are HOCs. They modify the List definition OR (more common) wrap the List in another component, that provides the service. Often they don't have names that remind you on a connection between the HOCs and the services they connect.
Famous example is Redux which is often used with in cooperation with react-redux to provide a React application with a Redux store. On the application root you got something like
<Provider store={store}>
<Application/>
</Provider>
And then somewhere there are some components defined like this:
const List = props =>
<ul>
{props.items.map(i => <li>{i}</li>}
</ul>
const ConnectedList = connect(state => ({items: state.items})(List)
Here the defined Redux store will be provided to the application and later it will be connected to a component. When seeing this the first time, it is not immediately obvious that they work together, that's why I wrote the first example more generic.
Anyway, what is happening here? Something gets provided, something gets connected and than what? How is it provided and connected?
Well, we know about the connect part, it's a HOC, it somehow connects the plain component with a service. Okay, but how "somehow"???
The HOC connect the service through the only interface components have, the props!
In the examples, the List needs an array in props.items, this is the only part of the, potentially big, props object it interacts with.
The next question is: Where does it get the service, which will be connected to the right prop?
From The Context!
I lied when I said, components just have the props interface. In fact they got two interfaces. The props and the context. The props always get delivered by the parent component. The context instead, can get delivered by any ancestor component. Like ServiceProviderA in our example.
But React now doesn't simply pass the context to every child component, you have to explicitly ask for it. And this asking is what you can implement in a HOC.
But first, how to get data into the context in the first place? How do these Provider components do it?
First the provider component, that you will use at your root, needs a getChildContext method, that will get service/data somehow. Second, it needs childContextTypes to tell React what it provides.
class ItemProvider extends React.Component {
// provide something usable, while the data is loading
state = {items: []}
// after added into the DOM, get some data
componentDidMount() {
getItemsFromService(this.props.url)
.then(items => this.setState({items}))
}
// if someone down the element tree needs our context data
// this method will be called
getChildContext() {
return {
items: this.state.items,
}
}
}
// This tells React that this component provides an array for components down
// the tree in context.items
ItemProvider.childContextTypes = {
items: React.PropTypes.array,
}
Now if we wrap some component with this provider, all the components down that tree can access its data without passing it through all of them manually via props. And this is access of the context is what we will do via HOC. So the HOC tells React, which part of the context it wants and then plugs it into the wrapped components props.
So how does the HOC look like?
const connectItems = (targetProp, WrappedComponent) => {
// the context will be passed as the second argument to a component function
const ItemComponent = (props, context) => {
// the connection happens here!
// props of this HOC will be merged with the items of the context
// and passed down to the wrapped component
const newProps = {...props, [targetProp]: context.items}
return <WrappedComponent {...newProps}/>
}
// This tells React, which part of the context this component wants
ItemComponent.contextTypes = {
items: React.PropTypes.array,
}
return ItemComponent
}
So, nothing new about HOCs here. The component tells React what it wants and then passes this data to its "wrapped" component. Here I used a stateless component definition for the HOC, because it doesn't do much, but you could use a component class too, if you need additional functionallity. Then the context would reside in this.context.
So to wrap it up, lets connect a component.
const List = props =>
<ul>
{props.items.map(i => <li>{i}</li>)}
</ul>
// first argument is the target prop
// it's where the items from context shold be passed to
const ConnectedList = connectItems('items', List)
Now we have a List component, that simply relies on an items prop, it can be tested easily and used with different data sources later.
And we have a ListWithItems that will render the List but also passes it data it got through the context.
Lets use this in an application:
<ItemProvider url={'/source/for/items'}>
<div>
<div>
<h2>A List of Items</h2>
<List style={{color: 'red'}} items={['x', 'y']}/>
<h2>A List of Server Items</h2>
<ConnectedList style={{color: 'blue'}}/>
</div>
</div>
</ItemProvider>
At the root of the application is the ItemProvider, that uses its url prop, to gather some items from a service after it was renderd.
Then the application has a bunch of div's for layout.
And somewhere deep inside the application the lists get rendered, one uses simply the data provided directly via props. The other one gets its data from the context. They can still use other props, like style or className.
Conclusion
HOCs are a handy way to keep special behaviour out of your components, make them simpler, more resuable and testable. Teamed up with context they provide an easy way for dependency injection like service provision.
If you're writing a medium to large sized React application, chances are you already met this pattern all over the place, but know you know how to implement it yourself.
Top comments (0)