Back to Blog
Lesson 12 of the Next.js: Build Full-Stack Apps with the App Router course
Next.jsJuly 30, 20264 min read

Loading States with loading.js in Next.js: A Practical Guide

Learn how to implement loading.js in Next.js to display skeleton screens. Improve your app's UX and perceived performance during asynchronous data fetching.

Next.jsUXperformanceReactweb development
Workers unloading goods from a truck in a rustic setting in Kolkata, India.

Previously in this course, we explored Managing Shared UI with Nested Layouts in Next.js. While layouts provide a consistent shell for your application, they don't solve the problem of what happens while your app is waiting for database responses or API calls. In this lesson, we add a critical layer to our user experience: automatic loading states.

The Role of Loading States in UX and Performance

When a user clicks a link, they expect an immediate response. If your server-side data fetching takes 500ms or more, the browser stays "stuck" on the previous page until the new content is ready. This lack of feedback makes your app feel sluggish.

Next.js simplifies this with the loading.js convention. By creating a loading.js file in any route segment, Next.js automatically wraps your page.js content in a React Suspense boundary. When the route loads, Next.js streams the loading UI immediately while the server fetches your data in the background.

This isn't just about showing a spinner; it's about perceived performance. By showing a skeleton screen—a static visual representation of your page structure—you signal to the user that content is on its way, which can significantly reduce bounce rates and frustration. If you are interested in broader strategies for managing UI during transitions, you might find our overview on Handling Loading States in React: Improving UX and Performance helpful for understanding the underlying principles.

Creating Your First loading.js File

To implement this, create a loading.js file at the same level as your page.js. Let’s assume we are working on our blog's post list page.

JSX
// app/blog/loading.js

export default function Loading() {
  // A simple skeleton screen
  return (
    <div className="space-y-4 animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/3"></div>
      <div className="space-y-2">
        <div className="h-4 bg-gray-200 rounded w-full"></div>
        <div className="h-4 bg-gray-200 rounded w-full"></div>
      </div>
    </div>
  );
}

When a user navigates to /blog, Next.js detects the loading.js file and renders it instantly. As soon as the page.js data fetch finishes, Next.js replaces the loading component with the actual page content.

Skeleton Screens and Layout Nesting

One of the most powerful features of the App Router is that loading.js respects the file system hierarchy. If you have a loading.js in a parent folder, it will apply to all nested routes unless they have their own specific loading.js.

FeatureBehavior
Scopeloading.js applies to the current route segment and its children.
HierarchyNested segments override parent loading states.
StreamingContent is streamed to the client, keeping the shell interactive.

This means you can have a general loading state for your entire blog section and a more specific, high-fidelity skeleton for individual post pages.

Hands-on Exercise: Implement a Post Skeleton

  1. Locate your existing app/blog/[slug]/page.js.
  2. Create an app/blog/[slug]/loading.js file.
  3. Design a skeleton that mimics the layout of your post (a title block and a few lines of text).
  4. Use Tailwind's animate-pulse utility class on your skeleton container to provide a subtle "shimmer" effect that signals activity.

Common Pitfalls

  • Over-designing the Skeleton: Keep your loading UI lightweight. If your loading.js file is too large or contains heavy images, you've just shifted the performance bottleneck from the data fetch to the loading UI itself.
  • Ignoring Layout Shifts: Ensure your skeleton matches the dimensions of the final content. If your skeleton is significantly smaller than the actual page, the content will "jump" into place when it loads, which triggers a Layout Shift (bad for Core Web Vitals). We discuss this further in Web Performance: Stop Layout Shifts with Smarter Font Loading.
  • Forgetting Suspense: Remember that loading.js is a convenience wrapper. For more complex scenarios, you can manually use <Suspense> inside your components to show loading states for specific parts of the page rather than the whole screen.

FAQ

Does loading.js work for client-side navigation? Yes. When using the Link component, Next.js triggers the loading.js file associated with the target route during navigation.

Can I use the same loading file for multiple pages? Yes, you can place a loading.js file in a parent route segment folder to share it across all child routes.

How do I test this if my local server is too fast? Add a await new Promise((resolve) => setTimeout(resolve, 2000)); at the top of your page.js to simulate a slow network response.

Recap

We've learned how to leverage loading.js to provide immediate visual feedback. By using skeleton screens, we bridge the gap between user interaction and data availability, creating a smoother, more professional experience for our blog readers.

Up next: Handling Errors with error.js — we'll learn how to catch runtime exceptions gracefully without crashing the entire page.

Similar Posts