Next.js 13/14: Loading States with File-based Routing

Overview
Imagine clicking a link and being met with a blank screen. Is the site loading? Did the link break? This frustrating experience is all too common, but Next.js 13 and 14 offer an elegant solution: file-based loading states. By simply adding a loading.js
or loading.tsx
file to your route segments, you can provide immediate visual feedback to your users, letting them know that content is on its way. This seemingly small detail can dramatically improve user experience and make your application feel more responsive and polished. Let's dive into how this works and how you can leverage it to create a better experience for your users.
Understanding File-Based Routing in Next.js
Next.js revolutionized React development with its file-based routing system. Instead of manually configuring routes, the framework automatically creates routes based on the directory structure within the app
directory. Each folder represents a route segment, and files within those folders define the content to be rendered. This intuitive approach simplifies route management and makes it easier to reason about your application's structure. To learn more about Next.js routing, refer to the official Next.js documentation.
How File-Based Routing Works
The core principle is that each folder inside the app
directory represents a route segment. For example, a folder named /blog
would correspond to the /blog
route. A file named page.js
or page.tsx
within that folder defines the component that will be rendered when a user visits that route. Nested folders create nested routes, allowing for complex application structures to be easily managed.
For instance, consider the following directory structure:
app/
├── layout.tsx
├── page.tsx
└── blog/ ├── [slug]/ │ └── page.tsx └── page.tsx
In this example, app/page.tsx
would render the homepage, app/blog/page.tsx
would render the blog index page, and app/blog/[slug]/page.tsx
would render individual blog posts based on the slug
parameter. The [slug]
syntax indicates a dynamic route segment.
Benefits of File-Based Routing
File-based routing offers several advantages over traditional routing methods:
- Simplicity: It's easy to understand and manage routes based on the file system.
- Convention over Configuration: Next.js handles the routing logic automatically, reducing boilerplate code.
- Dynamic Routes: Supports dynamic route segments for creating flexible and data-driven applications.
- Colocation: Route definitions are located alongside the components they render, improving code organization.
Introducing Loading States with loading.js
/loading.tsx
Next.js 13 and 14 introduce a special file convention for handling loading states: the loading.js
or loading.tsx
file. When placed within a route segment, this file automatically renders a loading UI while the route's content is being fetched or processed. This provides immediate feedback to the user, preventing the dreaded blank screen and improving the overall user experience.
The Purpose of Loading States
Loading states are crucial for providing a smooth and responsive user experience. Without them, users might be left wondering if the application is working, especially on slower network connections or when fetching large amounts of data. A loading indicator reassures users that the application is processing their request and that content will be displayed shortly.
How loading.js
/loading.tsx
Works
When a user navigates to a route segment containing a loading.js
or loading.tsx
file, Next.js will first render the component exported from this file. Once the actual page content is ready, Next.js will replace the loading UI with the final content. This happens seamlessly, providing a smooth transition from the loading state to the fully rendered page.
The loading.js
or loading.tsx
file should export a default React component that renders the loading UI. This component can be as simple as a spinner or as complex as a skeleton UI that mimics the structure of the page being loaded.
Creating a Basic Loading Indicator
Let's start with a simple example. Create a new Next.js project or use an existing one. Then, create a loading.tsx
file within a route segment, such as the app/dashboard
directory:
// app/dashboard/loading.tsx
import React from 'react'; const Loading = () => { return ( <div className="flex items-center justify-center min-h-screen bg-gray-100"> <div className="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div> </div> );
}; export default Loading;
This code defines a simple loading indicator using Tailwind CSS. It creates a spinner that rotates while the page is loading. Now, when you navigate to the /dashboard
route, you'll see this spinner while the actual dashboard content is being fetched.
Implementing Loading States in Next.js 13/14: A Step-by-Step Guide
Let's walk through the process of implementing loading states in a Next.js application. We'll cover creating a basic loading indicator, customizing it with different styles, and implementing more advanced loading UIs like skeleton loaders.
Step 1: Setting Up Your Next.js Project
If you don't already have a Next.js project, create one using the following command:
npx create-next-app@latest my-nextjs-app
Choose the options that best suit your needs. For this example, we'll use TypeScript, ESLint, Tailwind CSS, and the app
directory.
Step 2: Creating a Route Segment
Create a new folder within the app
directory to represent a route segment. For example, let's create a /products
route:
mkdir app/products
Step 3: Adding a page.tsx
File
Inside the app/products
directory, create a page.tsx
file that will render the content for the /products
route:
// app/products/page.tsx
import React from 'react'; const ProductsPage = async () => { // Simulate fetching data from an API await new Promise((resolve) => setTimeout(resolve, 2000)); return ( <div className="container mx-auto py-8"> <h1 className="text-2xl font-bold mb-4">Products</h1> <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <li className="bg-white rounded-lg shadow-md p-4"> <h2 className="text-lg font-semibold mb-2">Product 1</h2> <p className="text-gray-700">Description of Product 1.</p> </li> <li className="bg-white rounded-lg shadow-md p-4"> <h2 className="text-lg font-semibold mb-2">Product 2</h2> <p className="text-gray-700">Description of Product 2.</p> </li> <li className="bg-white rounded-lg shadow-md p-4"> <h2 className="text-lg font-semibold mb-2">Product 3</h2> <p className="text-gray-700">Description of Product 3.</p> </li> </ul> </div> );
}; export default ProductsPage;
This code simulates fetching data from an API using setTimeout
to introduce a delay. This will allow us to see the loading state in action.
Step 4: Creating a loading.tsx
File
Now, create a loading.tsx
file in the same app/products
directory:
// app/products/loading.tsx
import React from 'react'; const Loading = () => { return ( <div className="flex items-center justify-center min-h-screen bg-gray-100"> <div className="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div> </div> );
}; export default Loading;
This code defines the same basic loading indicator as before. When you navigate to the /products
route, you'll now see this spinner while the product data is being fetched.
Step 5: Running Your Application
Start your Next.js development server:
npm run dev
Open your browser and navigate to http://localhost:3000/products
. You should see the loading spinner for 2 seconds, followed by the product list.
Customizing Loading Indicators for Enhanced User Experience

While a basic spinner is functional, you can significantly improve the user experience by customizing the loading indicator to better match your application's design and branding. Let's explore some ways to customize loading indicators.
Using Different Loading Animations
There are many different loading animations available online, both in CSS and JavaScript. You can easily incorporate these into your loading component. For example, you could use a pulsating animation, a progress bar, or a custom SVG animation.
Here's an example of using a pulsating animation with Tailwind CSS:
// app/products/loading.tsx
import React from 'react'; const Loading = () => { return ( <div className="flex items-center justify-center min-h-screen bg-gray-100"> <div className="w-24 h-24 bg-blue-500 rounded-full animate-pulse"></div> </div> );
}; export default Loading;
This code creates a simple pulsating circle as the loading indicator.
Matching Your Brand with Custom Styles
You can further customize the loading indicator by using your application's brand colors and fonts. This will create a more cohesive and professional look. For example, if your brand uses a specific shade of green, you can incorporate that into the loading indicator.
// app/products/loading.tsx
import React from 'react'; const Loading = () => { return ( <div className="flex items-center justify-center min-h-screen bg-gray-100"> <div className="w-16 h-16 border-4 border-green-500 border-t-transparent rounded-full animate-spin"></div> </div> );
}; export default Loading;
This code changes the spinner color to green, matching a hypothetical brand color.
Implementing Skeleton Loaders for a Realistic Preview
Skeleton loaders are a more advanced technique that provides a realistic preview of the page being loaded. Instead of a generic spinner, skeleton loaders mimic the structure of the page, showing placeholders for text, images, and other content. This gives the user a better sense of what to expect and can make the loading experience feel faster.
Creating Skeleton Loaders in Next.js

Let's create a skeleton loader for our product list page. We'll create placeholders for the product titles and descriptions, giving the user a visual representation of the page structure while the data is loading.
Step 1: Designing the Skeleton UI
First, we need to design the skeleton UI. This will involve creating placeholder elements that mimic the structure of the product list. We'll use Tailwind CSS to style the placeholders.
// app/products/loading.tsx
import React from 'react'; const Loading = () => { return ( <div className="container mx-auto py-8 animate-pulse"> <h1 className="text-2xl font-bold mb-4 bg-gray-300 rounded w-1/4"></h1> <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <li className="bg-white rounded-lg shadow-md p-4"> <div className="h-6 bg-gray-300 rounded mb-2"></div> <div className="h-4 bg-gray-300 rounded"></div> </li> <li className="bg-white rounded-lg shadow-md p-4"> <div className="h-6 bg-gray-300 rounded mb-2"></div> <div className="h-4 bg-gray-300 rounded"></div> </li> <li className="bg-white rounded-lg shadow-md p-4"> <div className="h-6 bg-gray-300 rounded mb-2"></div> <div className="h-4 bg-gray-300 rounded"></div> </li> </ul> </div> );
}; export default Loading;
This code creates a skeleton UI with placeholders for the product list. The animate-pulse
class adds a subtle pulsing animation to the placeholders, making them more visually appealing.
Step 2: Implementing the Skeleton Loader
Replace the content of your app/products/loading.tsx
file with the code above. Now, when you navigate to the /products
route, you'll see the skeleton loader while the product data is being fetched.
Step 3: Customizing the Skeleton Loader
You can further customize the skeleton loader by adding more placeholders, adjusting the colors, and tweaking the animation. Experiment with different styles to create a loading experience that best matches your application's design.
Advanced Loading State Techniques
Beyond basic loading indicators and skeleton loaders, there are several advanced techniques you can use to further enhance the loading experience in your Next.js applications.
Route-Specific Loading States
Different routes may require different loading indicators. For example, a dashboard might benefit from a skeleton loader, while a settings page might only need a simple spinner. With Next.js's file-based routing, you can easily implement route-specific loading states by creating a loading.tsx
file in each route segment.
To illustrate this, let's create a different loading indicator for the /settings
route. First, create a /settings
directory:
mkdir app/settings
Then, create a page.tsx
file with some content:
// app/settings/page.tsx
import React from 'react'; const SettingsPage = async () => { // Simulate fetching data from an API await new Promise((resolve) => setTimeout(resolve, 1500)); return ( <div className="container mx-auto py-8"> <h1 className="text-2xl font-bold mb-4">Settings</h1> <p className="text-gray-700">Here you can manage your application settings.</p> </div> );
}; export default SettingsPage;
Finally, create a loading.tsx
file with a different loading indicator:
// app/settings/loading.tsx
import React from 'react'; const Loading = () => { return ( <div className="flex items-center justify-center min-h-screen bg-gray-100"> <p className="text-gray-500">Loading settings...</p> </div> );
}; export default Loading;
Now, when you navigate to /products
and /settings
, you'll see different loading indicators, each tailored to the specific route.
Handling Loading States for Specific Components
In some cases, you may need to handle loading states for specific components within a page, rather than the entire page. This can be achieved using React's state management capabilities and conditional rendering.
For example, let's say you have a component that fetches user data from an API. You can use the useState
hook to track the loading state of the component and conditionally render a loading indicator while the data is being fetched.
// app/components/UserComponent.tsx
import React, { useState, useEffect } from 'react'; const UserComponent = () => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchData = async () => { // Simulate fetching data from an API await new Promise((resolve) => setTimeout(resolve, 1000)); setUser({ name: 'John Doe', email: '[email protected]' }); setIsLoading(false); }; fetchData(); }, []); if (isLoading) { return <p>Loading user data...</p>; } return ( <div> <h2>User Profile</h2> <p>Name: {user.name}</p> <p>Email: {user.email}</p> </div> );
}; export default UserComponent;
This code defines a UserComponent
that fetches user data from an API. The isLoading
state variable is used to track the loading state of the component. While isLoading
is true, a loading message is displayed. Once the data is fetched, isLoading
is set to false, and the user data is displayed.
Optimizing Loading Performance
Loading performance is crucial for providing a fast and responsive user experience. There are several techniques you can use to optimize loading performance in your Next.js applications.
- Code Splitting: Next.js automatically splits your code into smaller chunks, allowing the browser to download only the code that is needed for the current page. This can significantly reduce the initial load time of your application.
- Image Optimization: Optimize your images by compressing them and using appropriate formats. Next.js provides built-in image optimization capabilities that can automatically optimize your images for different devices and screen sizes.
- Caching: Use caching to store frequently accessed data in the browser or on the server. This can reduce the number of requests that need to be made to the server, improving loading performance.
- Lazy Loading: Lazy load images and other resources that are not immediately visible on the screen. This can reduce the initial load time of your application and improve the user experience.
Best Practices for Implementing Loading States
Implementing loading states effectively requires careful consideration of user experience and performance. Here are some best practices to keep in mind:
- Provide Immediate Feedback: Always provide immediate feedback to the user when content is being loaded. This can be as simple as a spinner or as complex as a skeleton loader.
- Match Your Brand: Customize your loading indicators to match your application's design and branding. This will create a more cohesive and professional look.
- Use Skeleton Loaders: Consider using skeleton loaders to provide a realistic preview of the page being loaded. This can make the loading experience feel faster and more engaging.
- Optimize Performance: Optimize loading performance by using code splitting, image optimization, caching, and lazy loading.
- Test on Different Devices and Networks: Test your loading states on different devices and network conditions to ensure that they provide a good user experience for all users.
Common Pitfalls to Avoid
While implementing loading states is generally straightforward, there are some common pitfalls to avoid:
- Overusing Loading Indicators: Avoid displaying loading indicators for short loading times. This can be distracting and annoying for the user.
- Using Generic Loading Indicators: Avoid using generic loading indicators that don't match your application's design. This can make your application feel unprofessional.
- Ignoring Performance: Don't ignore loading performance. Optimize your code and assets to ensure that your application loads quickly.
- Not Testing on Different Devices: Don't forget to test your loading states on different devices and network conditions. This will ensure that they provide a good user experience for all users.
The Future of Loading States in Next.js
As Next.js continues to evolve, we can expect to see even more advanced features and capabilities for handling loading states. Some potential future developments include:
- More sophisticated skeleton loader APIs: Next.js could provide built-in APIs for creating skeleton loaders, making it easier to create realistic previews of pages being loaded.
- Automatic loading state detection: Next.js could automatically detect when content is being loaded and display a loading indicator without requiring developers to manually create a
loading.tsx
file. - Improved performance optimization: Next.js could provide even more advanced performance optimization techniques for reducing loading times and improving the user experience.
These potential developments would further simplify the process of implementing loading states and make it easier to create fast and responsive Next.js applications.
Conclusion
Implementing loading states with file-based routing in Next.js 13 and 14 is a game-changer for user experience. By simply adding a loading.js
or loading.tsx
file to your route segments, you can provide immediate visual feedback to your users, letting them know that content is on its way. This small detail can dramatically improve user satisfaction and make your application feel more polished and professional. From basic spinners to advanced skeleton loaders, the possibilities are endless. As Next.js continues to evolve, we can expect even more sophisticated tools and techniques for handling loading states, making it easier than ever to create fast and responsive web applications. So, embrace the power of file-based loading states and elevate your Next.js applications to the next level.