In React applications, it’s essential to provide users with feedback during data fetching, API calls, or other asynchronous tasks. A loading state is a useful tool to show users that the application is working on something in the background. This can enhance the user experience and prevent users from thinking the application is unresponsive.
Here’s how you can handle loading states in React.
1. Basic Loading State Example
The simplest way to handle a loading state is by using the useState
hook to track whether data is being loaded. Here’s an example that shows a loading spinner when data is being fetched.
Example with Fetch API
import React, { useState, useEffect } from 'react';
const FetchDataWithLoading = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true); // Initial state is loading
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result); // Set data after fetching
} catch (err) {
setError(err.message); // Set error message
} finally {
setLoading(false); // Set loading to false when the fetch is complete
}
};
fetchData();
}, []); // Empty dependency array ensures it runs once when the component mounts
if (loading) {
return <div>Loading...</div>; // Show loading message or spinner
}
if (error) {
return <div>Error: {error}</div>; // Show error message if any
}
return (
<div>
<h1>Data Fetched:</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default FetchDataWithLoading;
Explanation:
useState
:loading
is set totrue
initially, and once the fetch operation is done (whether successful or not), it’s set tofalse
.useEffect
: When the component mounts, it starts the fetch operation and handles the loading state.loading
state: While data is loading, it displays a loading message (“Loading…”) or spinner.
2. Displaying a Loading Spinner
Instead of just displaying a simple “Loading…” message, you can enhance the UX by showing a spinner. Here’s how to implement a loading spinner.
Example with a Spinner:
import React, { useState, useEffect } from 'react';
const LoadingSpinner = () => {
return <div className="spinner">Loading...</div>;
};
const FetchDataWithSpinner = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return <LoadingSpinner />; // Show the spinner while loading
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div>
<h1>Data Fetched:</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default FetchDataWithSpinner;
Styling the Spinner:
You can add some basic CSS to style the spinner:
.spinner {
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
This CSS creates a rotating circle spinner.
3. Using Libraries for Loading States
Instead of manually implementing a loading state or spinner, you can use libraries that provide pre-built components and loading indicators.
Example with React Loading Skeleton
React Loading Skeleton is a popular library that provides loading skeletons to mimic the structure of your UI during the loading state.
npm install react-loading-skeleton
Here’s how to implement it:
import React, { useState, useEffect } from 'react';
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';
const FetchDataWithSkeleton = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return (
<div>
<Skeleton height={30} count={5} /> {/* Skeleton loading for text */}
</div>
);
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div>
<h1>Data Fetched:</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default FetchDataWithSkeleton;
Explanation of Skeleton Loading:
Skeleton
component: This library automatically creates placeholder loading elements to mimic the structure of your content. It provides a smooth and user-friendly alternative to a simple loading message.count={5}
: This specifies that 5 skeleton elements (like loading lines) will be shown while the data is being fetched.
4. Handling Different Loading States
In some applications, you may have different loading states depending on the complexity of the operation. For instance, you might want to show different loading states for a list of items versus a detailed item.
Example of Conditional Loading States:
import React, { useState, useEffect } from 'react';
const ConditionalLoading = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [loadingDetails, setLoadingDetails] = useState(false);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://api.example.com/items');
const result = await response.json();
setData(result);
setLoading(false);
};
fetchData();
}, []);
const fetchDetails = async (itemId) => {
setLoadingDetails(true);
const response = await fetch(`https://api.example.com/items/${itemId}`);
const result = await response.json();
// Process result for the item
setLoadingDetails(false);
};
if (loading) {
return <div>Loading items...</div>;
}
return (
<div>
<h1>Items</h1>
<ul>
{data.map((item) => (
<li key={item.id}>
{item.name}
<button onClick={() => fetchDetails(item.id)}>
{loadingDetails ? 'Loading details...' : 'View details'}
</button>
</li>
))}
</ul>
</div>
);
};
export default ConditionalLoading;
Explanation:
loadingDetails
state: This shows a different loading state for fetching details of an item.- Conditional rendering: Depending on the loading state, you can display different loading messages or spinners.