Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Nextjs
  4. /Loading States
Your Progress0%
0 of 70 completed

NextJS Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication

Loading States with loading.tsx

Creating instant loading UI with automatic React Suspense boundaries

Nothing frustrates users more than staring at a blank screen wondering if something went wrong. Loading states tell users "we're working on it" and make your app feel responsive even when data takes time to load. Next.js makes this incredibly easy with loading.tsx files that automatically create React Suspense boundaries. Add a loading file, and Next.js handles the rest—showing instant loading UI while your page loads, then seamlessly swapping in the real content. Let's master this essential UX pattern!

What Is loading.tsx?

loading.tsx is a special file that defines loading UI for a route segment:

  • Automatic Suspense: Next.js wraps your page in React Suspense
  • Instant feedback: Shows immediately during navigation
  • Automatic replacement: Swaps to page content when ready
  • Works with streaming: Pages can stream in progressively

Loading States Structure

Each route segment can have its own loading.tsx

appImportant

Select a file or folder to see details

How It Works

When you create a loading.tsx file, Next.js automatically wraps your page like this:

TYPESCRIPT
<Layout>
  <Suspense fallback={<YourLoadingUI />}>
    <Page />
  </Suspense>
</Layout>

You just create the loading UI, Next.js handles the Suspense boundary automatically!

Creating Your First Loading State

Step 1: Simple Loading Spinner

app/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
    </div>
  );
}

That's it! Now when navigating to any page in your app, this loading UI shows first.

Step 2: Better Loading UI with Message

app/loading.tsx
export default function Loading() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mb-4" />
      <p className="text-gray-600 text-lg">Loading...</p>
    </div>
  );
}

Step 3: Styled Loading Component

app/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-50">
      <div className="text-center">
        {/* Animated spinner */}
        <div className="relative">
          <div className="animate-spin rounded-full h-20 w-20 border-t-4 border-b-4 border-blue-600 mx-auto" />
          <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
            <div className="h-10 w-10 rounded-full bg-blue-100" />
          </div>
        </div>
        
        {/* Loading text */}
        <h2 className="mt-6 text-xl font-semibold text-gray-800">
          Loading your content
        </h2>
        <p className="mt-2 text-gray-600">
          This won't take long...
        </p>
        
        {/* Animated dots */}
        <div className="mt-4 flex justify-center gap-1">
          <div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" />
          <div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce delay-100" />
          <div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce delay-200" />
        </div>
      </div>
    </div>
  );
}

🎨 Make It Match Your Design

Loading states should match your app's design system. Use the same colors, fonts, and style to maintain visual consistency.

Skeleton Screens

Better than spinners: skeleton screens that mimic your actual content layout:

Blog Post Skeleton

app/blog/[slug]/loading.tsx
export default function BlogPostLoading() {
  return (
    <div className="container mx-auto px-4 py-8 max-w-3xl">
      {/* Title skeleton */}
      <div className="h-12 bg-gray-200 rounded-lg w-3/4 mb-4 animate-pulse" />
      
      {/* Meta info skeleton */}
      <div className="flex gap-4 mb-8">
        <div className="h-4 bg-gray-200 rounded w-32 animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-24 animate-pulse" />
      </div>
      
      {/* Featured image skeleton */}
      <div className="aspect-video bg-gray-200 rounded-lg mb-8 animate-pulse" />
      
      {/* Content skeletons */}
      <div className="space-y-4">
        <div className="h-4 bg-gray-200 rounded animate-pulse" />
        <div className="h-4 bg-gray-200 rounded animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-5/6 animate-pulse" />
        <div className="h-4 bg-gray-200 rounded animate-pulse" />
        <div className="h-4 bg-gray-200 rounded animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-4/6 animate-pulse" />
      </div>
      
      {/* More content */}
      <div className="mt-8 space-y-4">
        <div className="h-4 bg-gray-200 rounded animate-pulse" />
        <div className="h-4 bg-gray-200 rounded animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-3/4 animate-pulse" />
      </div>
    </div>
  );
}

Dashboard Skeleton

app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="p-8">
      {/* Header skeleton */}
      <div className="mb-8">
        <div className="h-8 bg-gray-200 rounded w-64 mb-2 animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-96 animate-pulse" />
      </div>
      
      {/* Stats grid skeleton */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
        {[1, 2, 3].map((i) => (
          <div key={i} className="bg-white rounded-lg shadow p-6">
            <div className="h-4 bg-gray-200 rounded w-24 mb-3 animate-pulse" />
            <div className="h-8 bg-gray-200 rounded w-32 mb-2 animate-pulse" />
            <div className="h-3 bg-gray-200 rounded w-20 animate-pulse" />
          </div>
        ))}
      </div>
      
      {/* Chart skeleton */}
      <div className="bg-white rounded-lg shadow p-6">
        <div className="h-6 bg-gray-200 rounded w-48 mb-4 animate-pulse" />
        <div className="h-64 bg-gray-200 rounded animate-pulse" />
      </div>
    </div>
  );
}

Product Grid Skeleton

app/products/loading.tsx
export default function ProductsLoading() {
  return (
    <div className="container mx-auto px-4 py-8">
      <div className="h-10 bg-gray-200 rounded w-64 mb-8 animate-pulse" />
      
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
        {[...Array(8)].map((_, i) => (
          <div key={i} className="bg-white rounded-lg shadow overflow-hidden">
            {/* Image skeleton */}
            <div className="aspect-square bg-gray-200 animate-pulse" />
            
            {/* Content skeleton */}
            <div className="p-4 space-y-3">
              <div className="h-4 bg-gray-200 rounded animate-pulse" />
              <div className="h-4 bg-gray-200 rounded w-3/4 animate-pulse" />
              <div className="h-6 bg-gray-200 rounded w-24 animate-pulse" />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Why Skeleton Screens?

  • Perceived performance: Users feel content is loading faster
  • Reduce layout shift: Similar size to real content
  • Better UX: Shows what's coming, not just "loading"
  • Professional appearance: Looks more polished than spinners

Nested Loading States

Each route segment can have its own loading state, creating granular loading experiences:

PLAINTEXT
app/
  layout.tsx          ← Layout persists
  loading.tsx         ← Shows for all routes
  dashboard/
    layout.tsx        ← Dashboard layout persists
    loading.tsx       ← Shows for dashboard routes
    page.tsx          → /dashboard
    analytics/
      loading.tsx     ← Shows only for analytics
      page.tsx        → /dashboard/analytics

How Nested Loading Works

URL: /dashboard

PLAINTEXT
<RootLayout>
  <Suspense fallback={<DashboardLoading />}>
    <DashboardLayout>
      <DashboardPage />
    </DashboardLayout>
  </Suspense>
</RootLayout>

Shows: Dashboard loading while page loads

URL: /dashboard/analytics

PLAINTEXT
<RootLayout>
  <DashboardLayout>  ← Stays mounted
    <Suspense fallback={<AnalyticsLoading />}>
      <AnalyticsPage />
    </Suspense>
  </DashboardLayout>
</RootLayout>

Shows: Analytics loading while page loads
Dashboard layout stays visible!

⚡ Granular Loading

Nested loading states mean only the changing part shows a loading state. The layout above stays interactive and visible, providing better UX than full-page loading.

Streaming with Suspense

Next.js streams pages progressively. You can show partial content while other parts load:

Manual Suspense Boundaries

app/dashboard/page.tsx
import { Suspense } from 'react';
import { RevenueChart } from '@/components/RevenueChart';
import { RecentOrders } from '@/components/RecentOrders';

// Fast components load first
function QuickStats() {
  return (
    <div className="grid grid-cols-3 gap-4 mb-8">
      <div className="bg-white p-6 rounded shadow">
        <h3>Total Sales</h3>
        <p className="text-3xl">$12,345</p>
      </div>
      {/* More stats */}
    </div>
  );
}

// Loading fallback for slow components
function ChartSkeleton() {
  return <div className="h-64 bg-gray-200 rounded animate-pulse" />;
}

function OrdersSkeleton() {
  return <div className="h-96 bg-gray-200 rounded animate-pulse" />;
}

export default function DashboardPage() {
  return (
    <div className="p-8">
      {/* Shows immediately */}
      <QuickStats />
      
      {/* Shows loading, then streams in */}
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
      
      {/* Also streams independently */}
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

Result: QuickStats shows instantly, then chart and orders stream in independently as they're ready!

Streaming Benefits

  • Fast content shows immediately
  • Slow content doesn't block fast content
  • Better perceived performance
  • Users can interact with loaded parts

Common Loading Patterns

1. Page-Level Loading (Simplest)

app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="container mx-auto px-4 py-8">
      <div className="animate-pulse space-y-4">
        <div className="h-8 bg-gray-200 rounded w-3/4" />
        <div className="h-4 bg-gray-200 rounded" />
        <div className="h-4 bg-gray-200 rounded" />
        <div className="h-4 bg-gray-200 rounded w-5/6" />
      </div>
    </div>
  );
}

2. Component-Level Loading (Granular)

app/dashboard/page.tsx
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      {/* Each component loads independently */}
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      
      <Suspense fallback={<ChartSkeleton />}>
        <Chart />
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <Table />
      </Suspense>
    </div>
  );
}

3. Progressive Loading (Best UX)

app/products/page.tsx
import { Suspense } from 'react';

export default function ProductsPage() {
  return (
    <div>
      {/* Header shows immediately */}
      <header>
        <h1>Products</h1>
        <Filters />  {/* Client component - instant */}
      </header>
      
      {/* Products stream in */}
      <Suspense fallback={<ProductGridSkeleton />}>
        <ProductGrid />
      </Suspense>
    </div>
  );
}

4. Layout with Loading Content

app/blog/[slug]/page.tsx
import { Suspense } from 'react';

export default function BlogPostPage() {
  return (
    <article>
      {/* Post content loads */}
      <Suspense fallback={<PostSkeleton />}>
        <PostContent />
      </Suspense>
      
      {/* Comments load separately */}
      <div className="mt-12">
        <h2>Comments</h2>
        <Suspense fallback={<CommentsSkeleton />}>
          <Comments />
        </Suspense>
      </div>
    </article>
  );
}

Loading State Best Practices

1. Match the Layout

Skeleton screens should match your actual content structure:

TYPESCRIPT
// ✅ Good: Matches actual content
export default function Loading() {
  return (
    <div className="grid grid-cols-3 gap-4">
      {[1,2,3].map(i => (
        <div key={i} className="card">
          <div className="h-48 bg-gray-200 animate-pulse" />
          <div className="p-4 space-y-2">
            <div className="h-4 bg-gray-200" />
            <div className="h-4 bg-gray-200 w-3/4" />
          </div>
        </div>
      ))}
    </div>
  );
}

// ❌ Bad: Doesn't match
export default function Loading() {
  return <div>Loading...</div>;
}

2. Use Semantic Delays

TYPESCRIPT
// Show loading after a brief delay to avoid flashing
// (Only needed for very fast loads)

'use client';

import { useEffect, useState } from 'react';

export default function Loading() {
  const [show, setShow] = useState(false);
  
  useEffect(() => {
    const timer = setTimeout(() => setShow(true), 200);
    return () => clearTimeout(timer);
  }, []);
  
  if (!show) return null;
  
  return <div>Loading...</div>;
}

3. Provide Context

TYPESCRIPT
// ✅ Good: Clear what's loading
export default function Loading() {
  return (
    <div className="text-center py-12">
      <Spinner />
      <p>Loading your dashboard...</p>
    </div>
  );
}

// ✅ Better: Skeleton shows structure
export default function Loading() {
  return <DashboardSkeleton />;
}

// ❌ Bad: Generic and unclear
export default function Loading() {
  return <div>Loading</div>;
}

4. Keep It Simple

Loading states should be lightweight—they're temporary:

TYPESCRIPT
// ✅ Good: Simple, fast to render
export default function Loading() {
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-4 bg-gray-200 rounded" />
      <div className="h-4 bg-gray-200 rounded" />
    </div>
  );
}

// ❌ Bad: Too complex
export default function Loading() {
  // Don't fetch data in loading states
  // Don't use heavy animations
  // Don't import large dependencies
}

5. Accessibility

TYPESCRIPT
export default function Loading() {
  return (
    <div 
      role="status" 
      aria-live="polite"
      aria-label="Loading content"
    >
      <div className="spinner" />
      <span className="sr-only">Loading...</span>
    </div>
  );
}

Reusable Loading Components

Extract common loading UI into reusable components:

Skeleton Component

components/Skeleton.tsx
export function Skeleton({ 
  className = '',
  width = 'w-full',
  height = 'h-4',
}: {
  className?: string;
  width?: string;
  height?: string;
}) {
  return (
    <div 
      className={`${height} ${width} bg-gray-200 rounded animate-pulse ${className}`}
    />
  );
}

// Usage
import { Skeleton } from '@/components/Skeleton';

export default function Loading() {
  return (
    <div className="space-y-4">
      <Skeleton height="h-8" width="w-3/4" />
      <Skeleton height="h-4" />
      <Skeleton height="h-4" />
      <Skeleton height="h-4" width="w-5/6" />
    </div>
  );
}

Card Skeleton

components/CardSkeleton.tsx
import { Skeleton } from './Skeleton';

export function CardSkeleton() {
  return (
    <div className="bg-white rounded-lg shadow p-6">
      <Skeleton height="h-6" width="w-1/2" className="mb-4" />
      <Skeleton height="h-4" className="mb-2" />
      <Skeleton height="h-4" className="mb-2" />
      <Skeleton height="h-4" width="w-3/4" />
    </div>
  );
}

// Usage
export default function Loading() {
  return (
    <div className="grid grid-cols-3 gap-6">
      {[1,2,3].map(i => <CardSkeleton key={i} />)}
    </div>
  );
}

Spinner Component

components/Spinner.tsx
export function Spinner({ 
  size = 'md',
  className = '',
}: {
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}) {
  const sizeClasses = {
    sm: 'h-4 w-4 border-2',
    md: 'h-8 w-8 border-2',
    lg: 'h-12 w-12 border-4',
  };
  
  return (
    <div
      className={`${sizeClasses[size]} border-blue-600 border-t-transparent rounded-full animate-spin ${className}`}
      role="status"
      aria-label="Loading"
    >
      <span className="sr-only">Loading...</span>
    </div>
  );
}

Complete Practical Example

Blog Post Loading Skeleton

A complete skeleton that matches blog post layout

loading.tsx

Output Preview

Click "Run Code" to see the output

Key Takeaways

  • loading.tsx creates automatic Suspense boundaries - no manual setup
  • Shows instantly during navigation - better perceived performance
  • Each route segment can have its own loading state - granular control
  • Skeleton screens better than spinners - show structure, reduce shift
  • Combine with manual Suspense - for component-level streaming
  • Match your actual layout - similar structure and dimensions
  • Keep loading UI lightweight - it's temporary
  • Provide context and accessibility - tell users what's loading

What's Next?

You've mastered loading states—an essential part of great UX! Next, we'll explore error handling with error.tsx files. You'll learn how to create error boundaries, handle errors gracefully, provide recovery options, and build resilient applications that handle failures elegantly.

Error handling is just as important as loading states. Users need to know when something went wrong and what they can do about it. Let's make your app bulletproof!

⚡ Performance + UX

Loading states are about perceived performance, not just actual performance. Even if your app is fast, showing instant feedback makes it feel faster and more responsive. Invest in good loading states!

Test Your Understanding

Question 1 of 4

What does loading.tsx automatically create?

Master loading states in Next.js! Learn how to create instant loading UI and skeleton screens with automatic Suspense.

Previous
Templates vs Layouts
Next
Error Handling with error.tsx

Master Next.js Loading & Error Handling

Join 2,000+ developers building resilient Next.js apps. Get the next lesson on error handling delivered to your inbox - 100% FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

NextJS Tutorials

0 of 70 completed

Your Progress0%

Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo