Why do we need loading spinners in React JS?
- React JS is used to build dynamic applications where new data has to be loaded for every user input. When there is a delay in fetching the data from an external API, we need to display a loading spinner.
- Adding a loading spinner will potentially reduce cumulative layout shift which is one of the three core web vital metrics.
- Improve user retention as users might leave the page if they are not aware that the data is being loaded.
In this article, we will learn how to add loading spinner in a parent component that requires a loading screen to be displayed on API fetch. Loading spinners are considered good UI/UX practices as they inform the users that data are being loaded from an external API. Additionally, buttons must be disabled when the loading spinner is displayed to avoid multiple API calls during a fetch request.
In the below example, the user list is fetched from an external API on the button click. The Screen remains unchanged for a few seconds before displaying data immediately as a result of the delay in response from the external API. We now need to integrate the loading spinner component to be displayed during delays in between button click and response from API.
1. Create React component to display in loading spinner
The first step involves creating the loading spinner React component and importing the spinner.css file can be considered empty for now. The loading spinner component consists of two nested divs, the parent div as a container for the loading spinner and the child div for the spinner itself.
import React from "react";
import "./spinner.css";
export default function LoadingSpinner() {
return (
<div className="spinner-container">
<div className="loading-spinner">
</div>
</div>
);
}
2. Add CSS styles for loading spinner animation
Now it’s time to add styles to the loading spinner along with CSS animation for a rotating effect. The border-top and border attributes are used to specify the properties for the spinner and circle around the spinner. A border radius of 50% ensures that the spinner is in a circular shape.
Next using the CSS animation attribute, we define animation-name as “spinner” with duration and type of animation. The @keyframes selector is used to define the change in animation, in this case, we add transform with rotation from 0 to 360 degrees thus giving a continuous rotating effect for the spinner.
Learn more about the CSS loaders from w3schools.com/howto/howto_css_loader.asp. Find more CSS spinner styles from loading.io/css/ and icons8.com/cssload/
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-spinner {
width: 50px;
height: 50px;
border: 10px solid #f3f3f3; /* Light grey */
border-top: 10px solid #383636; /* Black */
border-radius: 50%;
animation: spinner 1.5s linear infinite;
}
3. Track loading using React state
We need a React state to access and update the loading state value dynamically so that the changes are reflected immediately on the screen. The default value of loading is false to hide the loading spinner initially.
const [isLoading, setIsLoading] = useState(false);
4. Set loading = true on button click
When the user clicks on the “Fetch Users” button, the handleFetch function is triggered to fetch the user List from API. We need to set the loading state to true before the fetch call in order to display a loading spinner as soon as the API call is triggered.
<button onClick={handleFetch}>Fetch Users</button>
const handleFetch = () => {
setIsLoading(true);
fetch("https://reqres.in/api/users?page=0")
.then((respose) => respose.json())
.then((respose) => {
setUsers(respose.data)
});
};
5. Disable buttons based on loading state
When the value of the isLoading state is true and the loading spinner is displayed, the API call that is in progress cannot be canceled. Hence we need to disable all the buttons when the value of isLoading state is true. Additionally, multiple clicks when the isLoading state value is true leads to necessary duplicate API calls which will be discarded once the server responds.
<button onClick={handleFetch} disabled={isLoading}>
Fetch Users
</button>
6. Handle success response from API, loading = false
Once API successfully returns a 200 response along with the user list, the loading spinner should no longer be displayed. Hence, setIsLoading(false) is called to hide the loading screen and display the user list. Additionally, by having isLoading = false, buttons are re-enabled allowing users to refresh the user list.
const handleFetch = () => {
setIsLoading(true);
fetch("https://reqres.in/api/users?page=0")
.then((respose) => respose.json())
.then((respose) => {
setUsers(respose.data)
setIsLoading(false) // Hide loading screen
});
};
7. Handle error response, loading = false
If the API returns an error instead of a success response, an error message has to be displayed and setIsLoading(false) to hide the loading spinner as the API has already responded.
We will create a state to display the error message and call setErrorMessage(message) along with setIsLoading(false) in the catch statement of the fetch API call.
// State to display error message
const [errorMessage, setErrorMessage] = useState("");
// Fetch API call with error handling
fetch("https://reqres.in/api/users?page=0")
.then((respose) => respose.json())
.then((respose) => {
setUsers(respose.data)
setIsLoading(false)
})
.catch(() => {
setErrorMessage("Unable to fetch user list");
setIsLoading(false);
});
return (
<div className="App">
{renderUser}
{errorMessage && <div className="error">{errorMessage}</div>}
<button onClick={handleFetch} disabled={isLoading}>
Fetch Users
</button>
</div>
);
8. Intergrate loading spinner in parent component
Now that the loading spinner component is created and isLoading states values are set correctly, we need to integrate the loading spinner component in the main App.js file.
First, we need to import “LoadingSpinner” component in App.js and add conditional rendering based on the isLoading state value. “<LoadingSpinner/>” component is rendered if isLoading is true and user list is displayed when isLoading is false.
import LoadingSpinner from "./LoadingSpinner";
return (
<div className="App">
{isLoading ? <LoadingSpinner /> : renderUser}
{errorMessage && <div className="error">{errorMessage}</div>}
<button onClick={handleFetch} disabled={isLoading}>
Fetch Users
</button>
</div>
);
Final code and Result
LoadingSpinner.js
import React from "react";
import "./spinner.css";
export default function LoadingSpinner() {
return (
<div className="spinner-container">
<div className="loading-spinner"></div>
</div>
);
}
LoadingSpinner.css
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-spinner {
width: 50px;
height: 50px;
border: 10px solid #f3f3f3; /* Light grey */
border-top: 10px solid #383636; /* Blue */
border-radius: 50%;
animation: spinner 1.5s linear infinite;
}
.spinner-container {
display: grid;
justify-content: center;
align-items: center;
height: 350px;
}
App.js
import React, { useState } from "react";
import LoadingSpinner from "./LoadingSpinner";
import "./styles.css";
export default function App() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const handleFetch = () => {
setIsLoading(true);
fetch("https://reqres.in/api/users?page=0")
.then((respose) => respose.json())
.then((respose) => {
setUsers(respose.data)
setIsLoading(false)
// Optional code to simulate delay
// setTimeout(() => {
// setUsers(respose.data);
// setIsLoading(false);
// }, 3000);
})
.catch(() => {
setErrorMessage("Unable to fetch user list");
setIsLoading(false);
});
};
const renderUser = (
<div className="userlist-container">
{users.map((item, index) => (
<div className="user-container" key={index}>
<img src={item.avatar} alt="" />
<div className="userDetail">
<div className="first-name">{`${item.first_name}
${item.last_name}`}</div>
<div className="last-name">{item.email}</div>
</div>
</div>
))}
</div>
);
return (
<div className="App">
{isLoading ? <LoadingSpinner /> : renderUser}
{errorMessage && <div className="error">{errorMessage}</div>}
<button onClick={handleFetch} disabled={isLoading}>
Fetch Users
</button>
</div>
);
}
styles.css
* {
margin: 0;
padding: 0;
}
.App {
display: flex;
height: 100vh;
flex-direction: column;
justify-content: center;
align-items: center;
}
.userlist-container {
display: grid;
justify-content: center;
align-items: center;
height: 350px;
}
.userlist-container {
grid-template-columns: 1fr 1fr;
gap: 5px 20px;
}
.user-container {
height: 100px;
display: flex;
border: 1px solid black;
}
.userDetail {
display: flex;
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
button {
margin-top: 10px;
border: 2px solid #383636;
color: #383636;
background-color: #fff;
cursor: pointer;
padding: 10px 20px;
font-family: "Lato", sans-serif;
}
button:hover {
background-color: #383636;
color: #fff;
transition: all 0.3s ease-in-out;
}
.error {
color: red;
}