Skip to content

Loading UI

loading.tsx lets you show an instant loading state for a route while its data-fetching Server Components are still resolving, without writing any manual loading state logic. This lesson explains how it works under the hood.

loading.tsx Is a Suspense Boundary

When you add a loading.tsx file next to a page.tsx, Next.js automatically wraps that page in a React <Suspense> boundary, using your loading.tsx component as the fallback. As soon as the user navigates to the route, Next.js shows the loading UI instantly, then swaps in the real page content once its data has finished loading on the server.

This means users get instant feedback on navigation — no blank screen while await fetch(...) calls resolve inside a Server Component — without you writing a single isLoading state variable.

// app/dashboard/loading.tsx
export default function Loading() {
  return <p>Loading dashboard...</p>;
}

// app/dashboard/page.tsx
export default async function DashboardPage() {
  const stats = await getStats(); // slow-ish fetch
  return <h1>{stats.totalUsers} users</h1>;
}

Visiting /dashboard shows "Loading dashboard..." immediately, then swaps to the real content once getStats() resolves.

loading.tsx File Convention

app/
  dashboard/
    loading.tsx    // fallback UI for this segment
    page.tsx        // the actual, potentially slow, page
  • loading.tsx must default-export a component with no required props.
  • It automatically applies to the page.tsx in the same folder and to any nested routes beneath it.
  • It has no effect if the page has no async data fetching that would actually suspend.
  • You can nest multiple loading.tsx files at different levels for more granular loading states.

Loading UI Cheat Sheet

How loading.tsx behaves across the route tree.

Scenario Behavior
loading.tsx in a route folder Shown instantly on navigation to that route
No loading.tsx present Browser waits with no visual feedback until content is ready
Nested loading.tsx files Each segment can show its own, more specific loading state
Underlying mechanism Automatic React <Suspense> boundary

Manual Suspense for Individual Components

loading.tsx covers an entire route segment, but you can also wrap individual slow components in your own <Suspense> boundary for more granular streaming — showing the rest of the page immediately while just one slow section loads in.

import { Suspense } from "react";

export default function DashboardPage() {
  return (
    <>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading stats...</p>}>
        <SlowStats />
      </Suspense>
    </>
  );
}

This pattern, called streaming, is covered in more depth in its own lesson later in this course.

Why This Improves Perceived Performance

Without loading.tsx, a slow Server Component leaves the browser showing the previous page (or a blank tab) until the entire new page's data resolves. loading.tsx gives immediate visual feedback the instant navigation starts, which measurably improves how fast an app feels even when the actual data fetch takes the same amount of time.

Common Mistakes

  • Expecting loading.tsx to show if the page has no async data fetching to actually suspend on.
  • Building a custom skeleton loader with useState/useEffect when loading.tsx already solves the problem declaratively.
  • Forgetting loading.tsx also applies to all nested routes beneath its folder, not just the immediate page.
  • Making the loading UI itself slow or heavy, defeating the purpose of instant feedback.

Key Takeaways

  • loading.tsx automatically wraps a route's page in a React <Suspense> boundary.
  • It shows instantly on navigation and is replaced once the page's data finishes loading.
  • It applies to the page in the same folder and to all nested routes beneath it.
  • You can also add manual <Suspense> boundaries around individual slow components for finer control.
  • Loading UI improves perceived performance even when actual load time is unchanged.

Pro Tip

Keep loading.tsx visually simple and fast to render — a lightweight skeleton or spinner — since it needs to appear instantly; save your more detailed, styled loading states for components wrapped in their own manual <Suspense> boundaries where you have finer control over timing.