loading.js
Shows instant fallback UI while a route segment streams in, via an automatic Suspense boundary.
export default function Loading(): React.ReactNode- Added in Next.js v13.0.0
Return value
React.ReactNodeThe fallback UI rendered while the segment's page and children are still streaming.
Changelog
- PHP v13.0.0loading introduced.
Dropping a loading file into a folder makes Next.js wrap that segment's page.js, its not-found.js and every nested layout.js in a React <Suspense> boundary, with this component as the fallback. You write no <Suspense> yourself. The fallback is prefetched, so it appears the instant the user clicks, and navigation stays interruptible — they can click away before the destination has finished loading.
Reach for it on any route that awaits data: it converts a dead click into a visible response. Prefer a skeleton that mirrors the real layout over a bare spinner.
What trips people up
- It cannot cover the layout in its own segment.
loading.jsnests insidelayout.js, so an uncached fetch or acookies()call in that layout blocks the navigation and no fallback is shown. Move the fetch into the page, or wrap it in its own<Suspense>. - It does not wrap
error.jsortemplate.jsin the same segment. - Streaming pins the status code to 200. Once the fallback has rendered, headers are already sent — a later
notFound()renders 404 UI but cannot change the status. Next.js injects<meta name="robots" content="noindex">so crawlers are not misled. - Static export does not support streaming.
- Some browsers buffer the first 1024 bytes, so trivial demo pages can look like nothing streamed.
Example
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <LoadingSkeleton />
}When to use it
- Show a dashboard skeleton while server-side queries for the page resolve
- Give a slow search results route an immediate visual response on click
- Render a cover image and title placeholder before an article's body streams in
- Test error and loading branches by toggling the boundary in React DevTools
Frequently asked questions
Why is my loading.js not showing?
Almost always because the awaiting code is in the layout of that segment rather than in the page. loading.js nests inside layout.js, so it cannot act as a fallback for the layout's own uncached fetches, cookies() or headers() calls — the navigation just blocks. Move the data fetch down into page.js, or wrap it in its own <Suspense> inside the layout.
Does loading.js work with a static export?
No. Streaming is unsupported for static exports, so the Suspense fallback never streams — the page is generated whole at build time. Node.js servers and Docker containers support it; on other platforms it depends on the adapter.
Why does my 404 page return a 200 status code?
Because streaming had already started when notFound() was called, and response headers cannot be changed once sent. Next.js adds <meta name="robots" content="noindex"> to the streamed HTML so search engines do not index it. To get a real 404 status, check that the resource exists before the body streams — for example in proxy.js.
More examples
A skeleton that mirrors the finished layout
Five placeholder rows stand in for the list the page will render, so the layout does not jump when real data arrives. The count is a fixed literal rather than derived from data — the fallback renders before any data exists, so it cannot depend on it.
export default function Loading() {
return (
<section aria-busy="true" aria-label="Loading dashboard">
<div className="skeleton-title" />
<ul>
{Array.from({ length: 5 }, (_, i) => (
<li key={i} className="skeleton-row" />
))}
</ul>
</section>
)
}Finer-grained boundaries inside the page
loading.js is all-or-nothing for the segment. Here the page itself splits into two boundaries, so a slow weather API no longer holds back the post feed — each region swaps in as its own data resolves, and the surrounding shell renders immediately.
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}Keeping the fallback visible while a layout fetches
TeamSwitcher reads cookies, which is runtime data. Left unwrapped in the layout it would block the navigation entirely and the segment's loading.js would never paint. Its own Suspense boundary lets the shell and the page's loading fallback appear immediately.
import { Suspense } from 'react'
import { TeamSwitcher } from './team-switcher'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<Suspense fallback={<div className="switcher-skeleton" />}>
<TeamSwitcher />
</Suspense>
{children}
</div>
)
}
Discussion