GioJSdocs
On this page

loading.tsx

A Suspense fallback for a folder and everything below it: a streamed page sends it first while the content suspends.

app/dashboard/loading.tsx
import React from 'react';

export default function Loading() {
  return <p aria-busy="true">Loading dashboard…</p>;
}

Reference

File name and location

loading.tsx, loading.jsx or loading.js, in app/ or any folder below it (route groups and dynamic folders included, private folders never). It covers its own folder and every folder below it; a deeper loading.tsx adds a boundary of its own inside it.

Props

None. The default export is rendered as <Loading />. Read the URL with usePathname() or useParams() if the fallback needs it.

Behavior

GioJS wraps the folder's content in <Suspense fallback={<Loading />}>, inside the folder's layout and its error.tsx boundary:

text
<DashboardLayout>        app/dashboard/layout.tsx
  <ErrorBoundary>        app/dashboard/error.tsx
    <Suspense>           app/dashboard/loading.tsx
      ...deeper layouts, then the page
  • Streamed pages (a GET of a page rendered per request) send the layouts and the loading UI first and the content when it resolves, in the same response. A HEAD request, and every page while an onResponse plugin is registered in gio.config.ts, is rendered completely before it is sent.
  • What it covers. Only what suspends while rendering: React's use() on a promise, a lazy() component. getServerSideProps runs before rendering starts, so the loading UI is never shown while it runs.
  • Cached pages (a page with revalidate) are rendered completely before they are stored, so visitors get the finished HTML and never see the loading UI. With shell = 'cache' the boundary is an edge of the cached shell: the shell holds the layouts and the loading UI, and the content streams per request.
  • Status codes. If the content throws or calls notFound() before it suspends, the answer is still the 500 or 404 page, exactly as without a loading.tsx. Once it has suspended, the 200 and the loading UI are already sent, so a later error is handled in the browser by the nearest error.tsx and the status stays 200.
  • Client navigation. The current page stays on screen until the next page's HTML has arrived; the loading UI shows only if the new page suspends in the browser.

Examples

Different fallbacks per section

text
app/dashboard/
  layout.tsx
  loading.tsx            # fallback for /dashboard and everything below
  page.tsx
  analytics/
    loading.tsx          # a chart skeleton for /dashboard/analytics only
    page.tsx

/dashboard/analytics gets both boundaries, nested: the dashboard layout stays on screen with the analytics skeleton inside it, because the inner boundary is the nearest one to the part that suspended.

A boundary around one part instead

loading.tsx replaces everything below its folder, the page included. When only one part of a page is slow, a <Suspense> of your own keeps the rest visible:

app/dashboard/page.tsx
import React, { Suspense } from 'react';
import { RevenueChart } from './revenue-chart';

export default function Dashboard() {
  return (
    <>
      <h1>Dashboard</h1>
      <Suspense fallback={<div className="skeleton" aria-busy="true" />}>
        <RevenueChart />
      </Suspense>
    </>
  );
}

The Streaming guide shows how a component suspends on data.

Good to know

  • A page that renders without suspending looks exactly as it would without the file: the boundary only adds React's Suspense markers to the HTML.
  • loading.tsx ships in the client bundle of every page below it, so it must be browser-safe.
  • gio routes lists the loading.tsx that applies to each page.
  • The loading UI is not a skeleton for getServerSideProps. If a page waits on slow data there, move that data into a promise the page renders with use(), or into a route.ts the browser fetches.

Version history

VersionChanges
v0.1.0-beta.8Introduced: loading.tsx wraps its folder in <Suspense>, in any folder of app/.