DEV Community

sudip khatiwada
sudip khatiwada

Posted on

Next.js Loading States Made Simple: A Beginner's Guide to Better UX

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

How it works:

  1. Start with loading: true
  2. Fetch data
  3. Set loading: false when done
  4. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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..." />
Enter fullscreen mode Exit fullscreen mode

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>}
Enter fullscreen mode Exit fullscreen mode

❌ Avoid This:

// Bad: Generic, unhelpful messages
{loading && <div>Loading...</div>}
{loading && <div>Please wait...</div>}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Always show loading states - Users need feedback
  2. Start simple - Use useState for basic needs
  3. Try Suspense - Great for Next.js 13+ App Router
  4. Be specific - "Loading posts..." is better than "Loading..."
  5. 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)