Learn how to handle loading states in Next.js with easy examples. Improve user experience in just 3 minutes!
Why Loading States Matter
When users click a button or visit a page, they need to know something is happening. Without loading states, your app feels broken or slow. Good loading states make your Next.js app feel professional and fast.
Method 1: Basic Loading with useState (Easiest Way)
This is the simplest way to show loading states in Next.js:
// components/UserCard.js
import { useState, useEffect } from 'react';
export default function UserCard() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch user data
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false); // Stop loading
});
}, []);
// Show loading spinner
if (loading) {
return <div className="text-center p-4">Loading user...</div>;
}
// Show user data
return (
<div className="border p-4 rounded">
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
);
}
How it works:
- Start with
loading: true
- Fetch data
- Set
loading: false
when done - Show loading message while
loading
is true
Method 2: Button Loading States
Perfect for forms and user actions:
// components/SaveButton.js
import { useState } from 'react';
export default function SaveButton() {
const [saving, setSaving] = useState(false);
const handleSave = async () => {
setSaving(true); // Start loading
// Simulate saving data
await new Promise(resolve => setTimeout(resolve, 2000));
setSaving(false); // Stop loading
alert('Saved!');
};
return (
<button
onClick={handleSave}
disabled={saving}
className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save Changes'}
</button>
);
}
Key points:
- Disable button while loading
- Change button text
- Always reset loading state
Method 3: React Suspense (Modern Way)
Perfect for Next.js 13+ App Router:
// app/users/page.js
import { Suspense } from 'react';
// Loading component
function Loading() {
return <div className="text-center p-8">Loading users...</div>;
}
// Main page component
export default function UsersPage() {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Users</h1>
<Suspense fallback={<Loading />}>
<UserList />
</Suspense>
</div>
);
}
// This component fetches data
async function UserList() {
// Fetch data (this runs on server)
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
return (
<div className="grid gap-4">
{users.map(user => (
<div key={user.id} className="border p-4 rounded">
<h3 className="font-bold">{user.name}</h3>
<p className="text-gray-600">{user.email}</p>
</div>
))}
</div>
);
}
Why Suspense is great:
- Automatic loading states
- Works with server components
- Cleaner code
- Built into React
Simple Loading Spinner Component
Create a reusable loading spinner:
// components/LoadingSpinner.js
export default function LoadingSpinner({ message = "Loading..." }) {
return (
<div className="flex items-center justify-center p-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
<span className="ml-2">{message}</span>
</div>
);
}
// Use it anywhere:
// <LoadingSpinner message="Loading posts..." />
Quick Tips for Better Loading States
✅ Do This:
// Good: Clear, specific messages
{loading && <div>Loading your profile...</div>}
{saving && <div>Saving changes...</div>}
{uploading && <div>Uploading image...</div>}
❌ Avoid This:
// Bad: Generic, unhelpful messages
{loading && <div>Loading...</div>}
{loading && <div>Please wait...</div>}
Handle All States:
function DataComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Always handle these 3 states:
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (data) return <div>Success! {data.name}</div>;
}
When to Use Each Method
Method | Best For | Difficulty |
---|---|---|
useState | Client-side loading, forms, buttons | Beginner |
Suspense | Server components, page loading | Intermediate |
Both | Complete apps with mixed needs | Advanced |
Complete Example: Simple Blog Post
Here's a complete example you can copy and use:
// app/posts/page.js
import { Suspense } from 'react';
export default function PostsPage() {
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Blog Posts</h1>
<Suspense fallback={<PostsLoading />}>
<Posts />
</Suspense>
</div>
);
}
function PostsLoading() {
return (
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="border p-4 rounded animate-pulse">
<div className="h-6 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded"></div>
</div>
))}
</div>
);
}
async function Posts() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
.then(res => res.json());
return (
<div className="space-y-4">
{posts.map(post => (
<article key={post.id} className="border p-4 rounded">
<h2 className="text-xl font-bold mb-2">{post.title}</h2>
<p className="text-gray-700">{post.body}</p>
</article>
))}
</div>
);
}
Key Takeaways
- Always show loading states - Users need feedback
-
Start simple - Use
useState
for basic needs - Try Suspense - Great for Next.js 13+ App Router
- Be specific - "Loading posts..." is better than "Loading..."
- Handle errors - Always plan for things going wrong
Loading states make your Next.js app feel professional and fast. Start with these simple examples and gradually add more complex patterns as you learn!
Ready to add loading states to your Next.js project? Start with the useState example - it works in any React component and takes just 5 minutes to implement.
Top comments (0)