Higher Order Component Tutorial
Today you will learn what are higher order components in React, do we still need them if we have React hooks and how to refactor higher order components in React hooks code.
Classes and high order components is an older approach to write React applications but you will still find a lot of React projects which still use classes or high order components. This is why you still need to know how to use them.
So here I already prepared a React component for us.
class Repos extends Component {
constructor() {
super();
this.state = {
isLoading: true,
data: [],
error: null,
};
}
componentDidMount() {
this.fetchData();
}
async fetchData() {
try {
const data = await fetch(props.dataSource);
const json = await data.json();
if (json) {
this.setState({
data: json,
isLoading: false,
});
}
} catch (error) {
this.setState({
isLoading: false,
error: error.message,
});
}
}
render() {
const { data, isLoading, error } = this.state;
return (
<ul>
{this.state.data.map(({ id, html_url, full_name }) => (
<li key={id}>
<a href={html_url} target="_blank" rel="noopener noreferrer">
{full_name}
</a>
</li>
))}
</ul>
);
}
}
So here we have a classic React component where we fetch some data on initialize, we show loading and error states and after getting data we render them on the screen.
Higher order component
Now the question is what does this code has to do with higher order components and what is it at all? Higher order component is a function which will return a function which will return a component. And it's difficult to understand this so we need to build an example.
As you can see Repos
class is a typical example of fetching data. We have isLoading
, data
and error
properties. Exactly the same approach we do again and again in every component where we want to fetch something. This is why it makes sense to move this logic outside of our component and make it reusable. Higher order component is a best approach in this case. This is why I created a file withDataFetching.js
. We prefix all our higher order components with with
and this is a code style which is typical for React.
// withDataFetching.js
const withDataFetching = props => WrapperComponent => {
class WithDataFetching extends Component {
}
return WithDataFetching
}
So here we created a function which gets props
argument as a parameter and return a function which has a WrapperComponent
as a parameter. Inside we create a React class component and return it. And the typical usage will look like this.
withDataFetching({url: ''})(Repos)
So we call withDataFetching
and pass url inside props
. This is out configuration of higher order component. After this we call our returned function on the component that we want to wrap. In our case it's Repos
component. Now let's move whole fetching logic to withDataFetching
.
// withDataFetching.js
import { Component } from "react";
const withDataFetching = (props) => (WrapperComponent) => {
class WithDataFetching extends Component {
constructor() {
super();
this.state = {
isLoading: true,
data: [],
error: null,
};
}
componentDidMount() {
this.fetchData();
}
async fetchData() {
try {
const data = await fetch(props.dataSource);
const json = await data.json();
if (json) {
this.setState({
data: json,
isLoading: false,
});
}
} catch (error) {
this.setState({
isLoading: false,
error: error.message,
});
}
}
render() {
const { data, isLoading, error } = this.state;
return (
<WrapperComponent
data={data}
isLoading={isLoading}
error={error}
{...props}
/>
);
}
}
return WithDataFetching;
};
export default withDataFetching;
As you can see it is mostly copy paste from Repos
component with a twist inside render
. What we want to do in render
is to return our WrapperComponent
which we got as an argument and provide inside all props that we got and additionally properties from withDataFetching
like data
, isLoading
, error
. So the goal of higher order component is to move logic outside of our component and pass needed values back inside our component as props.
Now let's clean our Repos
and use withDataFetching
on it.
// Repos.js
import withDataFetching from "./withDataFetching";
const Repos = ({ isLoading, error, data }) => {
if (isLoading) {
return "Loading...";
}
if (error) {
return error.message;
}
return (
<ul>
{data.map(({ id, html_url, full_name }) => (
<li key={id}>
<a href={html_url} target="_blank" rel="noopener noreferrer">
{full_name}
</a>
</li>
))}
</ul>
);
};
export default withDataFetching({
dataSource: "https://api.github.com/users/monsterlessonsacademy/repos",
})(Repos);
As you can see our Repos
component now just get 3 additional props from higher order component. Also as an export we return now withDataFetching
call which takes a url to fetch and our Repos
component. In browser everything is working exactly like before but we moved our fetching logic to higher order component.
Higher order component moves logic outside and makes it reusable between components.
React hooks approach
But this is not the modern way of doing things. Inside React we have now React hooks this is why we don't use higher order components in new projects anymore. And the question is actually "Why?". As you saw with higher order component we have quite strange syntax with returning a function of the function and really often we need to combine several higher order component together.
export default withHistory(withRouter(withDataFetching({
dataSource: "https://api.github.com/users/monsterlessonsacademy/repos",
})))(Repos);
This makes our code more complex and less maintainable.
The modern approach to replace higher order components is by creating custom React hooks. So let's move our logic from withDataFetching
to useDataFetching
as we prefix all our custom hooks with use
.
// useDataFetching.js
import { useState, useEffect } from "react";
export default (dataSource) => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const data = await fetch(dataSource);
const json = await data.json();
if (json) {
setIsLoading(false);
setData(json);
}
} catch (error) {
setIsLoading(false);
setError(error.message);
}
};
fetchData();
}, [dataSource]);
return { isLoading, data, error };
};
First of all we created a custom hook with just a single argument dataSource
which is a url to fetch data from. Inside we have 3 useState
calls to store data
, isLoading
and error
properties which we return at the end. We put all our fetching logic inside useEffect
because fetching data is a side effect and we trigger it when our dataSource
changes. On success and error we simply set data in our state.
Now let's refactor our component to work with our custom hook.
// Repos.js
import useDataFetching from "./useDataFetching";
const ReposHooks = () => {
const { error, isLoading, data } = useDataFetching(
"https://api.github.com/users/monsterlessonsacademy/repos"
);
if (isLoading) {
return "Loading...";
}
if (error) {
return error.message;
}
return (
<ul>
{data.map(({ id, html_url, full_name }) => (
<li key={id}>
<a href={html_url} target="_blank" rel="noopener noreferrer">
{full_name}
</a>
</li>
))}
</ul>
);
};
export default ReposHooks;
So at the beginning we just call useDataFetching
and pass a url inside. We get back our error
, data
, isLoading
which are just local properties that we can use. As you can see the code is much easier to understand and support.
Want to sharpen your React skills and succeed in your next interview? Explore my React Interview Questions Course. This course is designed to help you build confidence, master challenging concepts, and be fully prepared for any coding interview.
📚 Source code of what we've done