Higher Order Components
Higher Order Components (or HOC or HOCs for short) were, and still are, a central concept in React. They allow you to implement components with reusable logic and are loosely tied to Higher Order Functions from functional programming. These kind of functions take a function as a parameter and return another function. In terms of React, this principle is applied to components. Higher Order Components derive their name from those Higher Order Functions.
Let us look at an example to illustrate this concept better:
1
const withFormatting = (WrappedComponent) => {
2
return class extends React.Component {
3
bold = (string) => {
4
return <strong>{string}</strong>;
5
};
6
italic = (string) => {
7
return <em>{string}</em>;
8
};
9
render() {
10
return <WrappedComponent bold={this.bold} italic={this.italic} />;
11
}
12
};
13
};
Copied!
We have defined a function called withFormatting that takes in a React component. The function will return a new React component which in turn renders the component that was passed into the function and equips it with the props bold and italic. The component can now access these props:
1
const FormattedComponent = withFormatting(({ bold, italic }) => (
2
<div>
3
This text is {bold('bold')} and {italic('italic')}.
4
</div>
5
));
Copied!
Typically, Higher Order Components can be used to encapsulate logic. They relate closely to the concept of smart and dumb components. Smart components (which also encompass Higher Order Components) are used to display business logic, deal with API communication or behavioral logic. Dumb components in contrast mostly get passed static props and keep logic to a minimum (which is only used to for display-logic). For example, it might decide whether to show a profile image or, if it is not present, show a placeholder image instead. Sometimes, we also refer to Container Components (for Smart components) and Layout Components (for Dumb components).
But why do we categorize components this way? This strict divide into business logic and display logic enables component based development. It allows us to create layout components which do not know of possible API connections and only display data which is passed to them, no matter where it comes from. It also enables the business logic components to only concern themselves with business logic without caring about how the data is displayed in the end.
Assume we want to switch between a list and map view in a user interface. A container component will be in charge of gathering the data which is needed for the user and will pass them to the configurable layout component. As long as both components keep to the interface the developer has set up (think PropTypes), both components are easily interchangeable and can be tested and developed independently.
But enough of the theory. Let's look at an example. We are going to load a list of the 10 biggest cryptocurrencies and their current price. To obtain the data from the Coingecko API, we create a Higher Order Component which loads the data and passes it to the layout component:
1
const withCryptoPrices = (WrappedComponent) => {
2
return class extends React.Component {
3
state = {
4
isLoading: true,
5
items: [],
6
};
7
8
componentDidMount() {
9
this.loadData();
10
}
11
12
loadData = async () => {
13
this.setState(() => ({
14
isLoading: true,
15
}));
16
17
try {
18
const cryptoTicker = await fetch(
19
'https://api.coingecko.com' +
20
'/api/v3/coins/markets?vs_currency=eur&per_page=10'
21
);
22
const cryptoTickerResponse = await cryptoTicker.json();
23
24
this.setState(() => ({
25
isLoading: false,
26
items: cryptoTickerResponse,
27
}));
28
} catch (err) {
29
this.setState(() => ({
30
isLoading: false,
31
}));
32
}
33
};
34
35
render() {
36
const { isLoading, items } = this.state;
37
return (
38
<WrappedComponent
39
isLoading={isLoading}
40
items={items}
41
loadData={this.loadData}
42
/>
43
);
44
}
45
};
46
};
Copied!
Et voila! We have written an HOC to obtain the data of the crypto prices from coingecko.com. But the Higher Order Component itself is not enough to make this work: we also need to define a layout component to which we delegate the task of displaying the data.
In order to do that, we define a generic PriceTable component which does not know about which data exactly it displays: it could be current yoghurt prices from the local supermarket or cryptocurrency prices from a stock market. Thus, we have given it a very generic name, PriceTable:
1
const PriceTable = ({ isLoading, items, loadData }) => {
2
if (isLoading) {
3
return <p>Prices are being loaded. Please wait.</p>;
4
}
5
6
if (!items || items.length === 0) {
7
return (
8
<p>
9
No data available. <button onClick={loadData}>Try again!</button>
10
</p>
11
);
12
}
13
14
return (
15
<table>
16
{items.map((item) => (
17
<tr key={item.id}>
18
<td>
19
{item.name} ({item.symbol})
20
</td>
21
<td>EUR {item.current_price}</td>
22
</tr>
23
))}
24
<tr>
25
<td colSpan="2">
26
<button onClick={loadData}>Reload</button>
27
</td>
28
</tr>
29
</table>
30
);
31
};
Copied!
This component knows about three props: isLoading, to inform it which data is still being loaded, items, which represents an array of articles with their corresponding prices and loadData, a function which allows us to start another API request to obtain new data.
Both components act independently of one another. The PriceTable is not limited to showing cryptocurrency prices, and the withCryptoPrices component does not necessarily need to display its data in a PriceTable component. We managed to write two completely encapsulated and reusable components.
But how do we combine these two components now? We can simply pass the PriceTable component as a parameter to the withCryptoPrices HOC component. This will look like this:
1
const CryptoPriceTable = withCryptoPrices(PriceTable);
Copied!
Whenever an instance of the CryptoPriceTable is rendered, the Higher Order Component will trigger an API request in the componentDidMount() lifecycle method and pass its result to the PriceTable component. The PriceTable then only needs to concern itself with displaying the data:
1
ReactDOM.render(<CryptoPriceTable />, document.getElementById('root'));
Copied!
This opens up a number of opportunities for us. First of all, both components are able to be independently tested. I will provide a bit more information in a later chapter about how exactly we can test layout components with snapshot testing.
We also have the opportunity to combine other layout components with the withCryptoPrices HOC. To illustrate this, we are going to display the prices in CSV format. Our HOC will remain the same whereas the layout component can be implemented as such:
1
const PriceCSV = ({ isLoading, items, loadData, separator = ';' }) => {
2
if (isLoading) {
3
return <p>Prices are loaded, please wait.</p>;
4
}
5
6
if (!items || items.length === 0) {
7
return (
8
<p>
9
No data available <button onClick={loadData}>Try again</button>
10
</p>
11
);
12
}
13
14
return (
15
<pre>
16
{items.map(
17
({ name, symbol, current_price }) =>
18
`${name}${separator}${symbol}${separator}${current_price}\n`
19
)}
20
</pre>
21
);
22
};
Copied!
And just like that we have implemented our very first own CSV layout component. We check again whether the data is being loaded, then whether items are present. This could also be extracted into another HOC component as HOCs can be nested as many times as you like. In the end, they are all just functions which are passed as parameters to yet another function.
At last, we can render the output: we iterate through the list of items, pick the relevant properties name, symbol and current_price via Object Destructuring and then wrap the individual lines with a pre element to correctly display the end of the line.
In contrast to the PriceTable component, we have introduced another optional prop: separator - to tell the render component how many separating symbols it should use to display the data. This separator prop can be passed as a simple prop (as is common in JSX):
1
const CryptoCSV = withCryptoPrices(PriceCSV);
2
3
ReactDOM.render(<CryptoCSV separator="," />, document.getElementById('root'));
Copied!
However, by introducing this change in the CSV component, another change needs to made in the withCryptoPrices HOC. So far only the isLoading, items and loadData props are passed to the child component (WrappedComponent):
1
return (
2
<WrappedComponent
3
isLoading={isLoading}
4
items={items}
5
loadData={this.loadData}
6
/>
7
);
Copied!
In order to pass the separator prop which was defined in <CryptoCSV separator="," /> correctly to the PriceCSV component, we need to inform the HOC to also pass other remaining props to the WrappedComponent. You can either choose to explicitly pass other props or to only pass the remaining ones:
1
return (
2
<WrappedComponent
3
{...this.props}
4
isLoading={isLoading}
5
items={items}
6
loadData={this.loadData}
7
/>
8
);
Copied!
{...this.props}can be used to pass the remaining props to the child component using Spread Syntax from ES2015+.
Higher Order Components are a great way to "centralize" logic and structure applications better. Logic can be easily extracted from layout components which would further complicate such components. Even though they have been a fairly new concept in React, the concept itself is a very old one.
Higher Order Components are still widely used and there's nothing controversial about their usage. However, newer concepts to achieve the same or similar objectives have been introduced, of which many increase readability. These are Functions as a Child; the newer Context API which has been introduced in React in Version 16.3.0; and Hooks – which can be used inside of Function Components since 16.8.0. All of these will be explained in detail later.
Last modified 1yr ago
Copy link