09-nextjsTermsLevel_02loading.tsx

loading.tsx

Level 2 — App Router UI Elements A special file that automatically displays a fallback UI (like a spinner or skeleton) while the page.tsx in the same folder is busy fetching data on the server.


1. Prerequisites


2. Term Category

Routing & Layouts (Instant Loading UI Component): loading.tsx wraps route segment pages in React <Suspense> boundaries to stream instant fallback loading skeletons during data fetching.


3. Explanation

Environment Context

  • Server & Client

(1) Design Motivation — "Why did we design this?"

In modern Next.js, your page.tsx is usually an async Server Component.

export default async function Dashboard() {
  const data = await fetchDatabase(); // Takes 2 seconds
  return <div>{data}</div>;
}

If a user clicks a link to go to /dashboard, the server starts fetching the data. For 2 seconds, the user sees absolutely nothing happen. The browser just hangs. loading.tsx solves this instantly. Next.js will instantly display the UI defined in loading.tsx while the page.tsx finishes its await promises.

(2) The Syntax

It's just a standard React component.

// app/dashboard/loading.tsx
export default function DashboardLoading() {
  // You can return a spinner, text, or a complex Skeleton layout!
  return <div className="spinner">Loading your dashboard...</div>;
}

(3) How it works under the hood

Next.js takes your loading.tsx and automatically wraps your page.tsx inside a React <Suspense> boundary.

// What Next.js does automatically:
<Suspense fallback={<DashboardLoading />}>
  <DashboardPage />
</Suspense>

Because of this, the Layout (layout.tsx) remains fully interactive while the page.tsx inside it is loading! The user can click the Navbar, then immediately click another link on the Navbar before the page even finishes loading.


4. Common Mistakes & Pitfalls

Mistake 1: Putting the await in the Layout instead of the Page

The mistake: A developer writes an async Layout that takes 3 seconds to fetch user data, and expects loading.tsx to show up.

Why it's wrong: loading.tsx only wraps the page.tsx (and its children) inside the same folder. It does not wrap the layout.tsx! If your layout blocks rendering with an await, the loading.tsx will not trigger, and the user will stare at a dead screen. Golden Rule: Keep your layouts as fast and static as possible. Do your heavy async data fetching inside page.tsx so the loading.tsx skeleton can display properly.


Mistake 2: Confusing loading.tsx with Client-Side useState(true) Loading Spinners

The mistake: Writing manual if (loading) return <Spinner /> in page components instead of creating loading.tsx.

Why it's wrong: loading.tsx automatically creates a React Suspense boundary on the server, streaming immediate fallback UI to the browser while the page RSC resolves.

Incorrect:

// app/page.tsx
if (isLoading) return <Spinner />; // ❌ Manual client loading state!

Fix:

// app/loading.tsx
export default function Loading() {
  return <SkeletonLoader />; // Automatic Suspense fallback streaming
}

Mistake 3: Creating Heavy Heavy-Weight Loading Skeletons That Cause Visual Jitter

The mistake: Creating loading.tsx skeletons with structural dimensions that differ drastically from the final rendered page.

Why it's wrong: Mismatched loading skeleton dimensions cause noticeable Cumulative Layout Shift (CLS) when real data loads. Match skeleton layout dimensions closely to final page layouts.

Incorrect:

/* Loading skeleton height 100px vs final page height 600px -> Layout shift! */

Fix:

/* Align skeleton heights and grids with final page layout structure */

5. Practice Exercises

Exercise 1: Creating Route Loading Skeletons with loading.tsx

Scenario: Create app/dashboard/loading.tsx to render a skeleton UI while dashboard data loads.

Requirements:

  1. Export default React component in loading.tsx.
Answer

Implementation

// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="p-6 animate-pulse space-y-4">
      <div className="h-8 bg-gray-200 rounded w-1/4"></div>
      <div className="h-32 bg-gray-200 rounded"></div>
      <div className="h-32 bg-gray-200 rounded"></div>
    </div>
  );
}

Technical Explanation

  1. loading.tsx automatically wraps page.tsx in a React <Suspense> boundary.
  2. Renders instant loading skeleton UI on the client while Server Component data resolves.
  3. Improves perceived performance and First Contentful Paint (FCP).

Exercise 2: Implementing Granular Component Suspense Fallbacks

Scenario: Use inline <Suspense fallback={<Skeleton />}> inside page.tsx for fine-grained section loading instead of whole-page loading.tsx.

Requirements:

  1. Wrap slow component in <Suspense>.
Answer

Implementation

import { Suspense } from "react";
import AnalyticsCard from "./AnalyticsCard";

export default function DashboardPage() {
  return (
    <main className="p-6">
      <h1>Dashboard Overview</h1>
      <Suspense fallback={<div className="h-24 bg-gray-100 animate-pulse" />}>
        <AnalyticsCard />
      </Suspense>
    </main>
  );
}

Technical Explanation

  1. Inline <Suspense> allows static page content (heading, layout) to render instantly while slow widgets load.
  2. loading.tsx streams the whole page segment; inline <Suspense> streams individual widgets.
  3. Granular streaming UI design.

Exercise 3: Preventing Layout Shift with Animated Skeleton Placeholders

Scenario: Design skeleton loading shapes that exactly match the dimensions of the final loaded cards to prevent layout shifts.

Requirements:

  1. Match layout dimensions in skeleton CSS.
Answer

Implementation

export default function CardSkeleton() {
  return (
    <div className="w-full h-48 bg-slate-200 animate-pulse rounded-lg p-4">
      <div className="h-6 bg-slate-300 w-1/2 rounded mb-4" />
      <div className="h-4 bg-slate-300 w-full rounded mb-2" />
      <div className="h-4 bg-slate-300 w-3/4 rounded" />
    </div>
  );
}

Technical Explanation

  1. Skeletons with matching height/width prevent Cumulative Layout Shift (CLS) when final data arrives.
  2. Keeps browser layout stable during HTML stream insertion.
  3. Essential Core Web Vitals optimization technique.


7. Key Takeaways

  • loading.tsx defines the fallback UI shown while the route's Server Components are resolving their async operations.
  • It provides instant feedback to the user during navigation, preventing the app from feeling "frozen".
  • It automatically wraps the page.tsx in a <Suspense> boundary.
  • The layout.tsx remains fully visible and interactive while the loading.tsx is displayed inside it.
Built with LogoFlowershow