Communicating with a web server is a classic side effect in a React application. For this reason, you place these requests not directly in the component function, but in an effect hook.
import { useState } from 'react';
import './BooksList.css';
import { useEffect } from 'react';
function BooksList() {
const [books, setBooks] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
fetch('http://localhost:3001/books')
.then((response) => {
if (!response.ok) {
throw new Error('Request failed');
}
response.json();
})
.then((data) => {
setBooks(data);
})
.catch((error) => setError(error));
}, []);
if (error !== null) {
return <div>An error has occurred: {error.message}</div>;
} else if (books.length === 0) {
return <div>No books found</div>;
} else {
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>ISBN</th>
</tr>
</thead>
<tbody>
{books.map((book) => (
<tr key={book.id}>
<td>{book.title}</td>
<td>{book.author}</td>
<td>{book.isbn}</td>
</tr>
))}
</tbody>
</table>
);
}
}
export default BooksList;
The BooksList component has the books state, where you store the data for display. You also define an additional error state, which serves as a repository for any error messages that may occur.
You call the useEffect function with a callback function and an empty dependency array so that the data is loaded only when the component is mounted. If you forget this second parameter, you won’t notice it at first. But when you open the developer tools of your browser and look at the outgoing network requests, you’ll notice that not just one request is sent, but hundreds of them in no time.
That’s because in this case you start a server request, and when it responds you update the state, which in turn triggers the Effect callback, and so on, so you’re in an infinite loop. For this reason, you should look at the developer tools every now and then so that you’ll notice any unusual behavior of your application during development.
Inside the Effect callback, you call the fetch function of the browser with the address of the server interface. The result is a Promise object that maps the asynchronous operation. You can use the then method of this object to access the response object of the request. There you can also check if everything was OK with the response by checking the ok property of the response object.
If it contains the value false, you throw an exception in our case, which you’ll handle later. If the response doesn’t contain any error, you can process the response in the next step. You must keep in mind that HTTP is a streaming protocol, where the body of the response can be transmitted in several parts from the server to the browser.
For this reason, you must also consume the body separately. The browser’s Fetch API provides you with the json method for this purpose, which is also asynchronous. It allows you to decode the response and interpret it as a JSON object. The resulting Promise object then contains the server's response, which you write to the component's state. If the request was successful, you’ll see the book list in your browser.
Error Handling
What happens if the server doesn’t respond or you mistyped the interface URL? If you don’t provide for these cases, the browser will throw an exception and stops running your application. For this reason, the source code from the listing earlier handles these types of errors at a general level. To the chain of calls of the then methods of the Promise objects, you add the call of the catch method. If an error occurs when requesting the server or decoding the response, the browser executes the callback function of the catch method and writes the error object to the error state of the component.
To display the error, you must check that the error state no longer has a value of null when you render the component, and then display the message property of the error object. The type and scope of information you display to your users in the event of an error is entirely up to you. You should generally use error information sparingly or be careful with the content of error messages so as not to reveal too much to potential attackers on your application.
You can test whether your error-handling routine works by terminating the server process or specifying an invalid path such as booksX. In both cases, your application does not render the list, but displays the corresponding error message.
Using “async”/“await”
When using promises, as in the example case with server requests, you often use the async/await feature of the browser. It allows you to use promises, but without callback functions. This means you can streamline your source code and make it easier to read. The key is to add the async keyword to the function in which you use promises. Then you can prefix each asynchronous operation with the await keyword and get the promise value directly. The browser takes care of waiting for the result and doesn’t block the rest of your application either.
What you also need to know at this point is that an async function always returns a promise object. This fact makes integration with the Effect callback more difficult, as the React API dictates that this callback function should either return a function or nothing at all. You can still use async/await, but you have to create a helper construct for it. The code below shows the customized source code of the useEffect call. The rest of the component remains unchanged.
useEffect(() => {
(async () => {
try {
const response = await fetch('http://localhost:3001/books');
if (!response.ok) {
throw new Error('Request failed');
}
const data = await response.json();
setBooks(data);
} catch (error) {
setError(error);
}
})();
}, []);
To enable you to use async/await in the useEffect callback, you want to define an immediately invoked function expression (IIFE), an anonymous self-calling function in the form (async () => {...})(). This is a function that you define and call immediately. The async keyword allows you to use await. For error handling, you can then use a trycatch statement. The logic itself—calling fetch, checking and decoding the response, and setting the state and handling errors—remains the same. So if you switch to the browser with this customization, you will see no difference from the previous implementation. Which one you choose is up to you. But you should then consistently opt for your chosen solution instead of switching back and forth.
Server Communication with Axios
The Fetch API of the browser is the default tool when it comes to server communication. But especially for larger applications, this interface lacks some convenient features. Libraries such as Axios, which build on the Fetch API and add additional features, provide a workaround.
You install Axios in your application using the npm install url axios command. The additional installation of url package is required because all Webpack versions prior to version 5 included polyfills for node core packages. However, newer versions of Create React App use Webpack 5, which results in a bug when you use Axios. You can fix this by installing the URL package. Once installed, you can import Axios and use the library instead of the Fetch API in your components. The code below shows the deployment in the BooksList component:
import axios from 'axios';
…
useEffect(() => {
(async () => {
try {
const { data } = await axios.get(
`${process.env.REACT_APP_API_SERVER}/books`
);
setBooks(data);
} catch (error) {
setError(error);
}
})();
}, []);
As you can see in the code, Axios handles the correct decoding of the JSON response as well as error handling at this point. Like the Fetch API, Axios uses promises, so you only need to make minor adjustments to your source code.
However, a few saved lines do not justify the use of an additional library like Axios. This library provides even more features, such as the configuration of instances. This has the advantage that you can centrally configure an instance and provide it with information such as the baseURL or specific header fields and then use it anywhere in your application. This listing contains an example of such an Axios instance:
import { useState, useEffect } from 'react';
import './BooksList.css';
import axios from 'axios';
const client = axios.create({
baseURL: process.env.REACT_APP_API_SERVER,
});
function BooksList() {
const [books, setBooks] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
(async () => {
try {
const { data } = await client.get(`/books`);
setBooks(data);
} catch (error) {
setError(error);
}
})();
}, []);
if (error !== null) {
return <div>An error has occurred: {error.message}</div>;
} else if (books.length === 0) {
return <div>No books found</div>;
} else {
return (
<table>…</table>
);
}
}
export default BooksList;
If you use the instance in more than one component, you can swap it out to a separate file and access it from multiple places in your application.
Editor’s note: This post has been adapted from a section of the book React: The Comprehensive Guide by Sebastian Springer. Sebastian is a JavaScript engineer at MaibornWolff. In addition to developing and designing both client-side and server-side JavaScript applications, he focuses on imparting knowledge. He inspires enthusiasm for professional development with JavaScript as a lecturer for JavaScript, a speaker at numerous conferences, and an author. Sebastian was previously a team leader at Mayflower GmbH, one of the premier web development agencies in Germany. He was responsible for project and team management, architecture, and customer care for companies such as Nintendo Europe and Siemens.
This post was originally published in 6/2025.
Comments