Part 115: Enhancing User Experience with Global Loading Indicators in Next.js

[App] Streaming with Suspense

[App] Streaming with Suspense

In web development, ensuring a seamless and engaging user experience often involves managing how loading states are presented. This becomes particularly important when dealing with dynamic content that may take time to fetch or render. In this post, we'll explore how to enhance user interfaces in a Next.js application using Skeleton UIs and global loading indicators powered by React's Suspense and Streaming features.

Creating a Skeleton Component for Comments

Previously, we discussed implementing a Skeleton UI for a comments section, which serves as a visual placeholder until real data is available. This provides users with a sense of the page layout and keeps them engaged during loading times.

Why Use Skeletons?

Skeletons are visually more informative than traditional spinners or loading texts because they outline the content structure. However, for sections where data loads quickly, the benefit of using Suspense and Skeleton UIs may be minimal. In such cases, especially when dealing with static pages, the need for these components diminishes as the server often returns pre-rendered content.

Introducing Global Loading Indicators

For a comprehensive loading strategy, Next.js provides a feature to define a global loading indicator using a "loading file". This component acts as a universal fallback during slow page renders, providing a consistent user experience across different parts of the application.

Implementing a Global Loading Component

Here's how to create a global loading component:

  1. Create a Loading Component: In the app directory, create a loading.jsx file. This component will serve as the default loading indicator for the entire application.

  2. Export a Loading Indicator: Define a simple React component that returns a loading spinner or message.

Here's a simple implementation using a spinner:

// app/loading.jsx
import { ArrowPathIcon } from '@heroicons/react/24/outline';

export default function Loading() {
  return (
    <div className="flex justify-center py-6">
      <ArrowPathIcon className="animate-spin h-6 text-orange-700 w-6" />
    </div>
  );
}

How It Works

  • Automatic Usage: The loading.jsx component is automatically used by Next.js when any page component takes time to render. This is particularly useful for client-side navigation where data fetching might introduce delays.

  • Integrated with Streaming and Suspense: The global loading file leverages Streaming and Suspense, ensuring that users see the loading indicator during slow renders while the rest of the layout, like headers and footers, remains visible.

Demonstrating with the Review Page

To see this in action, we added an artificial delay to simulate slow loading:

// app/reviews/[slug]/page.jsx
export default async function ReviewPage({ params: { slug } }) {
  console.log('[ReviewPage] rendering', slug);
  // simulate delay:
  // await new Promise((resolve) => setTimeout(resolve, 3000));
  const review = await getReview(slug);
  if (!review) {
    notFound();
  }
  // ...rest of the component
}

By uncommenting the delay, you can observe the global loading indicator in action. The Loading component will display a spinner until the main content is ready.

Simulating Delays in Data Fetching

During development, simulating delays can be quite useful to test loading states and ensure that components like Skeleton UIs and loading indicators function as expected. In our example, we introduced a delay in the getComments function to mimic real-world scenarios where data fetching might take longer.

Implementing and Removing Simulated Delays

In the lib/comments.js file, we initially added a delay using setTimeout to simulate a slow network response. This was done to test how our application handled loading states:

// lib/comments.js
export async function getComments(slug) {
  // simulate delay:
  // await new Promise((resolve) => setTimeout(resolve, 3000));
  return await db.comment.findMany({
    where: { slug },
    orderBy: { postedAt: 'desc' },
  });
}

Here, the line await new Promise((resolve) => setTimeout(resolve, 3000)); was used to introduce a 3-second delay. By commenting out this line, we can easily enable or disable this delay to test how the application behaves under different loading conditions.

Why Simulate Delays?

  1. Testing Loading States: By simulating delays, developers can verify the appearance and behavior of loading indicators, such as spinners or Skeleton UIs.

  2. User Experience Optimization: Observing how the application responds during delays can help identify areas for improvement, ensuring that users have a smooth experience even when network speeds vary.

  3. Development and Debugging: It allows developers to replicate slow network conditions without relying on actual slow connections, making it easier to debug and optimize components.

Customizing Loading Indicators

While a global loading file is convenient, sometimes specific routes may require unique loading states. You can achieve this by creating separate loading.jsx files within route-specific directories. For instance, placing a loading.jsx under the reviews folder will ensure it's used only for routes matching that path.

Conclusion

Implementing global loading indicators in Next.js using the loading.jsx file offers a uniform approach to handling slow page renders. While Skeleton UIs are ideal for section-specific loading, a global indicator ensures a consistent fallback for the entire application. This flexibility allows developers to tailor the user experience, ensuring users are kept informed and engaged during content loading.

By leveraging these techniques, you can enhance user satisfaction and maintain a smooth interaction flow, even when dealing with dynamic or data-intensive pages.

Last updated