How to Use the Fetch API With React Higher Order Components
Fetching data for a website is a common use case so it's a good example for exploring the different ways we can reuse common functionality in a React app.
Our components will be able to fetch data without knowing the implementation details, so the Fetch API code could be replaced in future with Axios, GraphQL or some other data service without modifying the components that consume the fetch.
Common code reuse in React can be implemented using Higher Order Components, Render Props and Hooks. This article will tackle Higher Order Components and more articles will follow on those other methods.
What's a Higher Order Component (HOC)?
Here are the React docs for HOCs (it's well worth taking the time to read through the React Concepts and Advanced Guides properly if you haven't already).
A Higher Order Component is a function that takes a component as an argument and returns that component. This gives us the ability to perform some functionality before returning the component. This functionality can be common and reusable, in our example the fetching of data using the Fetch API.
Let's see the code and then talk about what's going on. We'll name our HOC withFetch
.
// withFetch.js
import React from 'react';
function withFetch(WrappedComponent, requestUrl) {
class WithFetch extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
// fetch functionality to populate this.state.data
render() {
return (
<WrappedComponent data={this.state.data} {...this.props} />
)
}
}
return WithFetch;
}
export default withFetch;
(If you want to see the HOC returning a functional component, it's at the bottom 👇 of this post).
That's the skeleton for our withFetch
HOC. Walking through it...
withFetch
will receive a component (we've called the parameterWrappedComponent
), and arequestUrl
for the first data fetch.- A new React component named
WithFetch
is created which has it's own state, lifecycle methods and render method. - The
WithFetch
state contains a data property where we'll store the data from the fetch - The
WithFetch
render method returns the component that was passed in (WrappedComponent
), and passes it the data ofWithFetch
as a prop. - The
{...this.props}
are also passed down through to theWrappedComponent
WithFetch
is returned.
Where is WithFetch
returned to though? How is this called? Let's have a Todos component that receives a list of Todos and outputs them...
// Todos.js
import React from 'react';
class Todos extends React.Component {
renderTodos() {
if (!this.props.data) return;
const todoItems = this.props.data.map(todo => (
{todo.title}
));
const todoList = (
<>
<h1>Todos</h1>
<ul>{todoItems}</ul>
</>
);
return todoList;
}
render() {
return <>{this.renderTodos()}>;
}
}
export default Todos;
The Todos component renders the todos if it receives this.props.data
.
So let's render this Todos component, wrapped in the HOC that performs the fetch.
Here's App.js, a component that displays the Todos component.
// App.js
import React from 'react';
import Todos from './Todos.js'
import withFetch from "./withFetch";
// create the Todos wrapped in the HOC
const TodosWithFetch = withFetch(
Todos,
"https://jsonplaceholder.typicode.com/todos"
);
function App() {
return (
<>
<TodosWithFetch />
</>
);
}
export default App;
Here's what's going on in App.js
- The
Todos
component is imported - The
withFetch
function is imported - We create a const
TodosWithFetch
that calls the functionwithFetch
(don't do this within the return or render method of App.js, it'll create a new instance every render) - We pass
withFetch
theTodos
component, along with the request URL string (you'd probably want to have your API Urls and paths imported from elsewhere, but I'm just using a string inline here for brevity). - As seen above in
withFetch
, thewithFetch
function returns a React component which renders theTodos
component we passed it, and this is assigned toTodosWithFetch
. TodosWithFetch
is output in theApp
component.
The data fetch is taking place in withFetch
using the requestUrl
we passed it, so let's perform that fetch now. Back to withFetch
...
// withFetch.js
import React from 'react';
function withFetch(WrappedComponent, requestUrl) {
class WithFetch extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
componentDidMount() {
if (requestUrl) {
this.fetchData(requestUrl);
}
}
fetchData = async (requestUrl) => {
this.setState({
data: []
});
try {
const response = await fetch(requestUrl);
if (response.ok) {
const data = await response.json();
this.setState({
data
});
} else {
throw new Error('Fetch request error');
}
} catch (err) {
// handle an error
}
};
render() {
return (
<WrappedComponent data={this.state.data} {...this.props} />
)
}
}
return WithFetch;
}
export default withFetch;
We've added two things here, the componentDidMount and fetchData methods.
fetchData
receives the requestUrl argument- In
fetchData
we reset the data state (we could also initialise other variables here like the loading and error states) - ...and wrap the fetch in a try catch block to catch errors
- Fetch isn't rejected for a 404 or 500 error status so it won't be caught by the try catch. For 404/500s it returns false for
response.ok
, so we check this to make sure the reponse was successful, otherwise we throw an error to be caught by the try catch. - If the response is ok we convert it to json and set the state data property
- This state change means the data is then passed to the
WrappedComponent
as thedata
prop, which in our case renders theTodos
component. componentDidMount
callsfetchData
ifrequestUrl
has a value
Handling Loading and Error States
As mentioned above, you can add an isLoading
or isError
property to the WithFetch
state, and set these when the fetch takes place. You can then set them back to true or false on success or error of the fetch.
// withFetch.js
class WithFetch extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
isLoading: false,
isError: false
};
}
// ...
fetchData = async (requestUrl) => {
this.setState({
data: [],
isLoading: true,
isError: false
});
try {
const response = await fetch(requestUrl);
if (response.ok) {
const data = await response.json();
this.setState({
data,
isLoading: false,
});
} else {
throw new Error('Fetch request error');
}
} catch (err) {
// handle an error
this.setState({
isLoading: false,
isError: true // or pass the err itself
});
}
};
render() {
return (
<WrappedComponent
{...this.state}
{...this.props} />
)
}
// ...
In the constructor the isLoading
and isError
state properties are set to false. isLoading
is set to true when fetchData
is called, and false again on fetch success or error. isError
is set to false on fetchData
call and set to true or false depending on the fetch response. These values are then passed to the WrappedComponent
by spreading {...state}
, where they can be handled accordingly.
These multiple state properties could be handled by using the React hook useReducer
, but I'll address this in an article for fetching data using Hooks (coming soon!).
Calling Fetch from the WrappedComponent
At the moment the fetch takes place on componentDidMount
, but what if we want to call a fetch from the wrapped component, based on user input for example?
We can pass down access to methods on the WithFetch
component as we've done with state and props. So our fetching data functionality can be passed as a prop to the WrappedComponent
, and called from Todos.js.
// withFetch.js
// ...
render() {
return (
<WrappedComponent
{...this.state}
{...this.props}
getData={(requestUrl) => this.fetchData(requestUrl)}
/>
)
}
// ...
The getData
prop passed to the WrappedComponent
is a function that takes a new requestUrl
as an argument and calls the fetchData
method with that requestUrl
.
This can be used by the Todos
component simply by calling the getData
prop.
// Todos.js
// ...
render() {
return (
<>
{this.renderTodos()}
<button
onClick={() =>
this.props.getData("https://jsonplaceholder.typicode.com/posts")
}
>
Load Data
</button>
>/>
);
}
// ...
I've added a button to the Todos
component that when clicked calls the getData
prop function with a new requestUrl
. This is a contrived example for brevity and not very useful as the requestUrl
is unchanging, but you can see how methods on the HOC can be called, and amend accordingly to more usefully respond to user input.
Debugging
The HOC returns a component named WithFetch
, but when you're debugging in React dev tools you might like this to be named relative to the component you've passed to the HOC. See the React docs for how to set the displayName property to something more appropriate.
Using Functional Components
The withFetch
HOC returns a React class component, but we can just as easily return a functional component.
// withFetch.js
import React, { useEffect, useState } from "react";
function withFetch(WrappedComponent, requestUrl) {
const WithFetch = (props) => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
if (requestUrl) fetchData(requestUrl);
}, []);
const fetchData = async (requestUrl) => {
setIsLoading(true);
setIsError(false);
try {
const response = await fetch(requestUrl);
if (response.ok) {
const data = await response.json();
setIsLoading(false);
setData(data);
} else {
throw new Error("Fetch request error");
}
} catch (err) {
setIsLoading(false);
setIsError(err);
}
};
return (
<WrappedComponent
data={data}
isLoading={isLoading}
isError={isError}
{...props}
getData={(requestUrl) => fetchData(requestUrl)}
/>
);
};
return WithFetch;
}
export default withFetch;
Let's run through the main differences between the functional and class components.
WithFetch
is assigned a function rather than a class- We use the
useState
hook for the holding and setting state, rather than the React class componentthis.state
andthis.setState
- We use the
useEffect
hook for implementing componentDidMount (note that like componentDidMount this only runs once as it's second argument is an empty array) - The previous class methods are changed to functions
- The props passed to the
WrappedComponent
are the constants that are returned from the{useState}
hook rather than spreading{...this.state}
in the class version - Using the functional component doesn't require any changes to how the HOC is called from
App.js
Other Fetching Data in React Articles
Check back if you're interested in using Render Props or React Hooks, I'll be adding them to the blog soon.
If you have any questions or comments or want to connect, you can follow me on Twitter, or sign up to the newsletter in the footer for front-end articles to your inbox 👇