Create Seamless Loading Experiences in Next.js With Skeleton Screens
When building web applications with Next.js, handling loading states effectively is critical for creating a smooth user experience. When you fetch data from an API, users often experience a delay before the content is fully loaded. To enhance the user experience during this period, you can use placeholders or skeleton screens. The 'react-loading-skeleton’ npm package is a perfect tool for this purpose. This blog will guide you through the best practices for handling loading states in Next.js using this package.
Why Use Skeleton Loading Screens?
Skeleton screens provide us with a cleaner and more modern alternative to the traditional ‘spinner’ method. Skeleton screens provide a low fidelity preview of the content that will appear once data is fetched from an endpoint. This approach keeps the users engaged and sets expectations for the content layout, leading to a more seamless experience.
A Step-by-Step Guide
Install the Necessary Packages
First you must install the ‘react-loading-skeleton’ npm package. This package will provide us with ready-to-use skeleton elements that you can easily integrate into your Next.js application.
Install either:
yarn add react-loading-skeleton
npm install react-loading-skeleton
Fetching Data From Our Serverless Function
Lets imagine we have a serverless function in our Next.js application called articles
, which will be called by hitting the URL/api/articles
in our fetch statement. This fetch will happen after the component rendered due to the fetch statement being in the useEffect
hook. Notice the empty dependency array which will only cause it to fire once. Until the fetch is complete, we would see nothing on the page.
import { useEffect } from 'react';
const HomePage = () => {
const [articles, setArticles] = useState([]);
useEffect(() => {
const fetchArticles = async () => {
try {
const response = await fetch('/api/articles');
const data = await response.json();
setArticles(data);
} catch (error) {
console.error('Error fetching articles:', error);
}
};
fetchArticles();
}, []);
return (
<div className="homepage">
articles.map(article => (
<div key={article.id} className="article">
<img src={article.image} alt={article.title} />
<h2>{article.title}</h2>
<p>{article.content}</p>
</div>
))
</div>
);
};
export default HomePage;
Article Skeleton Component
Lets first create a low fidelity/skeleton view of the layout of an individual article. We will save this in a separate file called SkeletonArticle
and import it into our component that will be fetching the article data.
// components/SkeletonArticle.tsx
import Skeleton from 'react-loading-skeleton'
const SkeletonArticle = () => {
return (
<div className="skeleton-article">
<Skeleton height={64} width={64} count={1} /> // this will mock out our image
<Skeleton height={20} width={120} count={1} /> // this will mock out our title
<Skeleton height={16} count={3} /> // this will mock out 3 lines of body content
</div>
);
};
export default SkeletonArticle;
Loading useState Variable
Lets add a useState
variable that will indicate that the component has not rendered yet. We will call it ‘loading’ and it will initially be set to true
as the component will not have any data when the component first loads. We will also be importing our SkeletonArticle
here. We will display three instances of these components to show the user what the expected layout will be and to indicate that we are currently fetching data.
import { useState, useEffect } from 'react';
import SkeletonArticle from '../components/SkeletonArticle';
const HomePage = () => {
const [articles, setArticles] = useState<Articles>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchArticles = async () => {
try {
const response = await fetch('/api/articles');
const data = await response.json();
setArticles(data.articles);
} catch (error) {
console.error('Error fetching articles:', error);
} finally {
setLoading(false);
}
};
fetchArticles();
}, []);
return (
<div>
{loading && (
<>
<SkeletonArticle />
<SkeletonArticle />
<SkeletonArticle />
</>
) : (
articles.map((article, index) => (
<div key={index} className="article">
<img src={article.image} alt={article.title} />
<h2>{article.title}</h2>
<p>{article.content}</p>
</div>
))
)}
</div>
);
};
export default HomePage;
Further Considerations
Consistent Design of Screens
Ensure your skeleton screens match the design and layout of the actual content. This consistency helps users understand what to expect once the content loads.
Error Handling
Handle errors gracefully. If the API call fails, inform the user with a friendly message and provide options to retry or navigate to a different part of the application. We are just using a console log in our example but a developer could have an additional useState flag that is set to either an error returned from the API or a generic message if the API fails. Then we would display this message upon such failure.
const [apiErrorOccured, setApiErrorOccured] = useState(false);
...
// in the fetch
if (response.status !== 200) {
setApiErrorOccured(true);
}
...
// in the return statement
{apiErrorOccured && (
<div>An error has occured! Please try again later.</div>
)}
Enhance Next.js Loading States With Effective Skeleton Screens
Using skeleton screens to handle loading states in Next.js applications significantly improves user experience by providing visual feedback during data fetching. By following these best practices and integrating the ‘react-loading-skeleton’ npm package, you can create a more engaging and seamless experience for your users.