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.

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.
| Feature | Behavior |
|---|---|
| Scope | loading.js applies to the current route segment and its children. |
| Hierarchy | Nested segments override parent loading states. |
| Streaming | Content 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
- Locate your existing
app/blog/[slug]/page.js. - Create an
app/blog/[slug]/loading.jsfile. - Design a skeleton that mimics the layout of your post (a title block and a few lines of text).
- Use Tailwind's
animate-pulseutility 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.jsfile 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.jsis 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.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Next.js Website & Landing Page Development
A blazing-fast, SEO-optimized website or landing page in Next.js — the kind that loads instantly and ranks. Design-to-code, done right.
