loading.tsx
A Suspense fallback for a folder and everything below it: a streamed page sends it first while the content suspends.
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:
<DashboardLayout> app/dashboard/layout.tsx
<ErrorBoundary> app/dashboard/error.tsx
<Suspense> app/dashboard/loading.tsx
...deeper layouts, then the page- Streamed pages (a
GETof a page rendered per request) send the layouts and the loading UI first and the content when it resolves, in the same response. AHEADrequest, and every page while anonResponseplugin 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, alazy()component.getServerSidePropsruns 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. Withshell = '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 the500or404page, exactly as without aloading.tsx. Once it has suspended, the200and the loading UI are already sent, so a later error is handled in the browser by the nearesterror.tsxand the status stays200. - 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
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:
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.tsxships in the client bundle of every page below it, so it must be browser-safe.gio routeslists theloading.tsxthat 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 withuse(), or into aroute.tsthe browser fetches.
Related
- Layouts & Pages: Loading UI
- Streaming
- Error Handling: streaming and loading.tsx
- error.tsx, layout.tsx, shell
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Introduced: loading.tsx wraps its folder in <Suspense>, in any folder of app/. |