Why
When we create a Single Page Application (SPA), we often need to integrate APIs. Sometimes third-party APIs, but at least our own back-ends to get the data we need to display. These APIs are based on the HTTP or WebSocket protocol, each having its requirements for connection setup and tear down.
In this article, I explain the basic integration of HTTP APIs.
What
HTTP is a stateless protocol. It's the simplest way to get data from the server.
- call the fetch function
- get a promise
- wait until the promise resolves
- update the application with the new data
Sometimes HTTP requests fail, and sometimes we cancel them because the data hasn't arrived yet but isn't needed anymore.
Lifecycle Methods
Lifecycle methods are component methods with special names that are called by React to specific events.
For example, componentDidMount
will be called after React rendered a component into the DOM.
Hooks
Hooks are a new part of React and allow us to do things we did with lifecycle methods, but without the need to create a component class, they work with function components only.
For example, the callback supplied to the useEffect
hook function will be called every time React rendered a component.
How
First, let us integrate via lifecycle methods.
Lifecycle Methods
To use lifecycle methods, we need to create a class component that has three methods, render
, componentDidMount
and componentWillUnmount
.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
data: null,
error: null
};
}
async componentDidMount() {
this.abortController = new AbortController();
try {
const response = await fetch(API_URL, {
signal: this.abortController.signal
});
if (response.status >= 300)
throw new Error(response.statusText);
const data = await response.json();
this.setState({ loading: false, data });
} catch (e) {
if (e.name != "AbortError") this.setState({ error: e.message });
}
}
componentWillUnmount() {
this.abortController.abort();
}
render() {
const { data, error, loading } = this.state;
if (!!error) return <h2>{error}</h2>;
if (loading) return <h2>Loading...</h2>;
return <h2>{data}</h2>;
}
}
Let's go through this step-by-step.
1 - Define the state in the constructor
For an HTTP request, we need three states. loading
, data
and error
.
2 - Start the request in the componentDidMount
lifecycle method
We use an asynchronous function here, so we can handle the promises of the fetch
function with await
.
First, we need to define an AbortController that allows us to cancel the HTTP request. Then we call fetch
in a try
block and await
its response
.
We also pass the signal
of the abortController
into the fetch call to wire up the controller with the request. This signal is used to cancel the request when we call the abort
method of the abortController
.
If the status
of our request isn't an error code, we assume the data is ready to be parsed; we add it to our state and set loading
to false
.
If the fetch
call throws an error, we get an error code from the server, or the abort
method of the abortController
is called, we catch
the error and render an error.
3 - Cancel the request in componentWillUnmout
the lifecycle method
Since we saved a reference to the abortController
to this
, we can use it in the componentWillUnmount
method. This method is called by React right before the component gets removed from the DOM.
The call to abort
leads to a rejection of the fetch
promise.
In the catch
block we only call the setState
method if the error isn't an AbortError
because we know that React will remove our component from the DOM.
4 - Render the different states
Finally, we have to render the different states. The main logic is inside the lifecycle methods, so the render
method doesn't need much logic anymore.
Hooks
To use hooks, we have to create a functional component. In the function we have to use two hooks, useState
to store our state and useEffect
to handle the API call.
function MyComponent() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
useEffect(async () => {
const abortController = new AbortController();
try {
const response = await fetch(API_URL, {
signal: abortController.signal
});
if (response.status >= 300)
throw new Error(response.statusText);
const data = await response.json();
setData(data);
setLoading(false);
} catch (e) {
if (e.name != "AbortError") setError(e.message);
}
return () => abortController.abort();
}, []);
if (!!error) return <h2>{error}</h2>;
if (loading) return <h2>Loading...</h2>;
return <h2>{data}</h2>;
}
1 - First setup the state with the useState
hook
The useState
hook takes an initial value and returns a new state-variable and a setter function. Every time the setter is called, it will cause React to re-render the component with the new value inside the state-variable.
2 - Start the request with the useEffect
hook
The useEffect
hook takes a callback that is called every time React renders the component (i.e. when we call a setter function).
When we pass an empty array as the second argument to useEffect
the callback is only executed after the first render. This allows us to emulate the behavior of the componentDidMount
lifecycle method.
The logic in the callback is mostly the same as in the lifecycle method example. The main differences are the missing this
, because we don't have a class component and that we use the setters of the useState
hooks.
3 - Cancel the request with the returned function
The function we return from the callback supplied to the useEffect
hook is executed before the component is removed from the DOM. This allows us to emulate the behavior of the componentWillUnmout
lifecycle method.
We call the abort
method of the abortController
and are done.
4 - Render the different states
To render we can use the state-variables returned by the useState
hooks. Most of the logic is inside the callback we passed to useEffect
so not much to do here.
API Analytics & Monitoring
Btw, if you curious about how to add API analytics to your React SPA, check out this example.
Conclusion
The two ways to integrate API calls into React components are mostly a matter of taste. Some people prefer an object-oriented approach; others want to be more functional. React lets you go either way, both allow for error handling and cancelation.
Moesif is the most advanced API Analytics platform, supporting REST, GraphQL and more. Thousands of API developers process billions of API calls through Moesif for debugging, monitoring and discovering insights.
Learn More
Originally published at www.moesif.com.
Top comments (4)
Nice post K! I think one thing that seems very subtle is how the abort is handled with hooks example. That seems a bit implicit, is there any way to make that operation more explicit?
You mean to give the user of the hook a way to call the abort manually?
I'm not sure if what I am asking fits into that question exactly. I guess I'm wondering if in the
hooks
world there is the explicit unmounting that the abort could be called in. It seems a bit odd thatuseEffect
implicitly mimicks the unmount and would only then execute the callback function.This is likely a knowledge gap in my React, but I was hoping you might be able to provide a clearer take on it.
Ah, okay.
You supply a callback to the useEffect hook and this callback can return another callback.
The first callback is called on a mount event, the returned callback is the one that is called on an unmount event.
(If you supply an array as second argument to useEffect, the first callback is also executed if one of the elements intl this array changed between renders, if the array is empty, the first callback is only called in mount, like with my example)