Loading spinners in React is 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.
There are two ways to create a loading spinner in a React.js project:
- Using a custom loading spinner
- Using react-loader-spinner library
Method 1: Using a custom loading spinner
Customizing the loading spinner requires a separate CSS file and component file.
The main logic behind loading the Spinner is simple: when you try to perform an async action, you will show the Spinner until the action is completed.
Let’s start our tutorial step-by-step:
Step 1: Create a React.js project
You can create a new project using the Vite template using this command:
npm create vite@latest react-app -- --template react
Go inside the project and install the dependencies:
cd react-app npm install
Start the development server using this command:
npm run dev
Step 2: Modify the src/main.jsx file
Add the below code inside src/main.jsx file:
import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import App from "./App.jsx"; ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <App /> </React.StrictMode> );
We removed unnecessary code that comes by default with a project.
Step 3: Create a Spinner component
This spinner component shows a spinner-type SVG or Graphic. We will add animation and graphics through CSS. Add the below code inside the src/index.css file:
.spinner { border: 4px solid rgba(0, 0, 0, 0.1); width: 36px; height: 36px; border-radius: 50%; border-left-color: #09f; animation: spin 1s ease infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
Now, create a new component called Spinner.jsx inside the src folder:
Add the below code inside Spinner.jsx file:
import "./index.css"; const Spinner = () => <div className="spinner"></div>; export default Spinner;
This component will be displayed when some asynchronous process occurs in our application.
Step 4: Fetching data from an API
We will use the built-in fetch() method to send a GET request to https://jsonplaceholder.typicode.com/posts, returning the JSON response.
We will use the setTimeout() method to mimic the delay in fetching the response. While delaying, we will display the spinner loading, and after waiting for 2 seconds, we will display the data fetched from an API.
In a nutshell, we will create a button, and when that button is clicked, we will wait for 2 seconds to display the loading spinner and, after 2 seconds, display the data.
Add the below code inside the App.jsx file:
import { useState } from "react"; function App() { const [data, setData] = useState([]); const [error, setError] = useState(null); const fetchData = async () => { setError(null); try { const response = await fetch( "https://jsonplaceholder.typicode.com/posts/" ); if (!response.ok) { throw new Error("Failed to fetch data"); } const res = await response.json(); setData(res); } catch (error) { setError("Network error: " + error.message); } }; const handleFetchClick = () => { setTimeout(fetchData, 2000); }; return ( <div style={{ padding: "20px" }}> <button onClick={handleFetchClick}>Fetch Data</button> {error ? ( <div style={{ color: "red" }}>Error: {error}</div> ) : ( <ul> {data && data.map((post) => ( <li key={post.id}> <h3>{post.title}</h3> <p>{post.body}</p> </li> ))} </ul> )} </div> ); } export default App;
In this code, we use the useState hook to initialize the state variables data and error.
This component returns a button with a click event. When the button is clicked, it triggers the handleFetchClick() function, which calls the setTimeout() function and delays for 2 seconds.
After 2 seconds, it will call the fetchData() function, which will send a network request to a server and fetch the data. If there is any error while fetching, we will display that error; otherwise, we will display the data.
Right now, we display nothing while waiting for 2 seconds, which is not good for User experience. We can improve this UX by adding a spinner loading.
Step 5: Importing a Spinner component
Here are the steps to perfectly integrate the loading spinner in our application:
- Initialize the loading state
- Send the network request
- Update the loading state
- Display the loading spinner
- Fetch the data
- Remove the loading spinner
import { useState } from "react"; import Spinner from "./Spinner"; function App() { const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [error, setError] = useState(null); const fetchData = async () => { setError(null); try { const response = await fetch( "https://jsonplaceholder.typicode.com/posts/" ); if (!response.ok) { throw new Error("Failed to fetch data"); } const res = await response.json(); setData(res); } catch (error) { setError("Network error: " + error.message); } finally { setLoading(false); } }; const handleFetchClick = () => { setLoading(true); setTimeout(fetchData, 2000); }; return ( <div style={{ padding: "20px" }}> <button onClick={handleFetchClick}>Fetch Data</button> {loading ? ( <Spinner /> ) : error ? ( <div style={{ color: "red" }}>Error: {error}</div> ) : ( <ul> {data && data.map((post) => ( <li key={post.id}> <h3>{post.title}</h3> <p>{post.body}</p> </li> ))} </ul> )} </div> ); } export default App;
Save this file and go to this URL: http://localhost:5173/
Method 2: Using the react-loader-spinner library
Another way is to use the react-loader-spinner library.
You can install this library using this command:
npm install react-loader-spinner --save
After installing it, you can import various loaders based on your requirements.
You can find all the components in the official documentation.
Let’s use the Audio loading spinner.
Update the src/App.jsx file with the below code:
import { useState } from "react"; import { Audio } from "react-loader-spinner"; function App() { const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [error, setError] = useState(null); const fetchData = async () => { setError(null); try { const response = await fetch( "https://jsonplaceholder.typicode.com/posts/" ); if (!response.ok) { throw new Error("Failed to fetch data"); } const res = await response.json(); setData(res); } catch (error) { setError("Network error: " + error.message); } finally { setLoading(false); } }; const handleFetchClick = () => { setLoading(true); setTimeout(fetchData, 2000); }; return ( <div style={{ padding: "20px" }}> <button onClick={handleFetchClick}>Fetch Data</button> {loading ? ( <Audio height="40" width="40" color="#4fa94d" ariaLabel="audio-loading" wrapperStyle={{}} wrapperClass="wrapper-class" visible={true} /> ) : error ? ( <div style={{ color: "red" }}>Error: {error}</div> ) : ( <ul> {data && data.map((post) => ( <li key={post.id}> <h3>{post.title}</h3> <p>{post.body}</p> </li> ))} </ul> )} </div> ); } export default App;
Save this file and check the output:
You can see that our loading spinner looks like an audio wave.
If there is something wrong, then it will display an error like this:
That’s it!
ramon calvo
Hi KRUNAL, I have a comment. in the line:
this.state = this.state = {name:”,company:”,
blog: ”,
avatar:”,
loading: false
loading have to be true. right?
because then in the resolve of promisse is when this state will change and the loader spin will disappear for render the json. tell me if I am wrong.
Jzamit
You are wrong my friend, never mutate the state, if you need to change it, always use this.setState({name: “My New Name”});
Clayton
He is not wrong.. He never said “mutate” the state, but rather it should be initialized with a true value, as the page is loading when we begin. You should call this.setState({loading: false}) after the data has been fetched.
pranay
hello,
i want set loader on submit button API fetching time
Abhivirus
Hey Krunal:
In the constructor(){
super();
this.state={
loading:true //to load the loader else loader pic will never displayed;
}
}
Braulio
Great article, we have created a microlibrary to avoid having to manually set the loading indicator to true/false, and keep count of multiple fetch calls, hope it helps: https://github.com/Lemoncode/react-promise-tracker a post about how it works: https://www.basefactor.com/react-how-to-display-a-loading-indicator-on-fetch-calls