Best Practices for Loading States in Next.js

Enhancing user experience with skeleton loading screens when fetching data from an API in Next.js

August 13, 2024

By Mike Payne

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.



Mike Headshot

Mike Payne

Development Team Lead

Mike is a Development Team Lead who is also Sitecore 9.0 Platform Associate Developer Certified. He's a BCIS graduate from Mount Royal University and has worked with Sitecore for over seven years. He's a passionate full-stack developer that helps drive solution decisions and assist his team. Mike is big into road cycling, playing guitar, working out, and snowboarding in the winter.

Second CTA Ogilvy's Legacy

Today, David Ogilvy's influence can still be felt in the world of advertising.

Ogilvy's Influence Example
Emphasis on research Market research is a crucial part of any successful advertising campaign
Focus on headlines A strong headline can make the difference between an ad that is noticed and one that is ignored
Use of visuals Compelling images and graphics are essential for capturing audience attention