In a Next.js App Router route segment, what does adding a loading.tsx file cause the framework to do, and which part of the UI does its output replace while data loads?
answer
- a file, not a component you render
- the framework wraps something for you
- automatic Suspense boundary per segment
- sits under the layout, over the page
- also fires on client navigation
basics
~20 sNext.js wraps that segment's page.tsx and everything below it in a Suspense boundary whose fallback is loading.tsx's default export. The layout above stays on screen, and the fallback also appears instantly on client navigation into the route.
solid answer
~50 s`loading.tsx` is a file convention, not a component you render yourself. When Next.js builds the tree for a segment, it nests the parts in a fixed order: `layout.tsx` wraps a Suspense boundary, that boundary wraps `page.tsx` and any nested segments below it, and the default export of `loading.tsx` becomes that boundary's fallback. So while the page's data is pending, the user sees the layout — nav, sidebar, chrome — with the loading UI sitting where the page would be, rather than a blank document. Because the boundary is a real Suspense boundary, the server can flush that shell as the first chunk and stream the page's HTML in when its data resolves. The same fallback shows on client-side navigation into the route, so the app responds to the click immediately instead of hanging on the old page. One `loading.tsx` covers its whole segment; a nested segment can add its own for finer scope.
code
tsx · 24 lines// app/dashboard/loading.tsx
export default function Loading() {
return <p>Loading dashboard…</p>;
}
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<nav>Dashboard nav stays visible</nav>
{children}
</section>
);
}
// app/dashboard/page.tsx
export default async function Page() {
const stats = await fetch('https://example.com/api/stats').then((r) => r.json());
return <h1>{stats.title}</h1>;
}go deeper
Recall that dropping a loading.tsx into a route folder is enough — no imports, no wiring — and that its default export appears where the page would be while the layout stays put.
Be able to draw the nesting: layout outside, the Suspense boundary from loading.tsx inside it, page inside that. Explain that this placement is why the shell can be flushed before the page's data resolves.
Expect to justify placement across a real app: one coarse root skeleton versus per-segment skeletons shaped like their content, and how nested files override rather than stack. Note that the layout renders above the boundary, so slow work there is not covered.
Own the convention as a team-wide default: where skeletons live, how faithful they must be to final layout, and when the automatic segment-level boundary is too coarse to be worth having at all versus hand-placed boundaries.
## The convention in one line `loading.tsx` (or `.js`/`.jsx`) placed in a route folder tells Next.js: wrap this segment's page in a Suspense boundary and use my default export as the fallback. You never import it, never render it, and never import `Suspense` yourself — putting the file in the folder is the whole API. ```tsx // app/dashboard/loading.tsx export default function Loading() { return <div className="skeleton">Loading dashboard…</div>; } ``` ## Where the boundary sits in the tree This is the part interviewers actually probe. For a segment, Next.js composes the conventional files in a fixed nesting order — simplified, it looks like this: ```tsx <Layout> {/* layout.tsx */} <ErrorBoundary> {/* error.tsx */} <Suspense fallback={<Loading />}> {/* loading.tsx */} <Page /> {/* page.tsx, plus nested segments below */} </Suspense> </ErrorBoundary> </Layout> ``` Two consequences follow directly: 1. **The layout is above the fallback**, so it renders and stays visible while the page is pending. Persistent chrome does not flash or remount. 2. **The boundary covers the page and everything nested under it**, so it is segment-granular: one fallback for the entire page area, not per component. ## Why the file exists: streaming A Suspense boundary is what lets the server send HTML in pieces. Next.js renders and flushes everything above the boundary as the initial shell — `<html>`, the layout, the fallback markup — while the page's async work is still running. When the page's data resolves, its HTML is streamed in and swaps into place. Without a boundary, the server has nothing it can safely commit to the wire until the whole tree is ready, so the browser waits on a blank response. Note what this does and does not buy. The total work is unchanged; the slow query still takes as long as it takes. What changes is that the first byte and first paint no longer wait for the slowest thing on the page. That is a perceived-performance win, and it is also a real one for metrics that measure when pixels appear. ## The second job: instant navigation state `loading.tsx` is not only for the first server render. On a client-side navigation to the route, Next.js shows the same fallback immediately when the user clicks, then swaps in the real content when it arrives. Navigation stays interruptible — the user can click something else without waiting. This is why a route can feel unresponsive on click even when the server is fast: with no boundary, the browser has nothing new to show until the whole payload lands. ## Scope, nesting, and precedence A `loading.tsx` applies to its own segment and every segment below it that does not define its own. Add one at `app/dashboard/loading.tsx` and it covers `/dashboard`, `/dashboard/settings`, and the rest — until `app/dashboard/settings/loading.tsx` introduces a nearer boundary, which then takes over for that subtree. Placement is therefore a real decision: a root-level `loading.tsx` gives you a single app-wide skeleton, which is coarse but cheap; per-segment files give each area a skeleton shaped like its own content. ## What it is not - **Not an error boundary.** Errors thrown during render are handled by `error.tsx`, a separate convention. A failed fetch does not show the loading UI forever; it propagates to the error boundary. - **Not a global spinner.** It is tied to one segment's boundary, not to "any pending request anywhere in the app." A fetch fired inside a Client Component after hydration does not trigger it. - **Not required for streaming.** It is a shorthand. You can achieve the same thing — and finer granularity — by putting `<Suspense>` around slow subtrees inside `page.tsx`. The file convention just gives every segment a default boundary for free. - **Not a way to make the page arrive sooner.** Anything the framework must render *above* the boundary still blocks the shell. ## The mental model to carry into the interview Say it as placement, not as a feature: `loading.tsx` is a Suspense boundary at a known position in the segment tree — under the layout, over the page. Everything else about its behavior (layout stays, page area swaps, fallback on navigation, nested override) falls out of that one fact.
- If both a parent segment and a nested child segment define loading.tsx, which fallback does the user see?The nearest boundary above the pending work wins. Navigating straight to the child route puts the child's `loading.tsx` in charge of the child's page area, while the parent's fallback covers the parent segment itself — for example when you navigate into the parent and its own page has not resolved yet. Nested files override, they do not stack.
- Does loading.tsx also handle a component that throws while rendering?No. Suspense and error handling are separate concerns with separate conventions: `loading.tsx` supplies a Suspense fallback, `error.tsx` supplies an error boundary. A rejected fetch does not leave the skeleton up forever; it propagates to the nearest `error.tsx`.
- Does a route need a loading.tsx file in order to stream?No. Streaming comes from Suspense boundaries, and `loading.tsx` is just a per-segment shorthand for one. You can put `<Suspense>` directly in `page.tsx` or in a layout and get streaming with no `loading.tsx` at all — often with better granularity, since you choose exactly which subtree waits.
It is like a picture frame that stays hung on the wall while the print inside is being swapped: the surrounding chrome never moves, only the panel it encloses changes.
saying these in an interview costs you the question
- Calls it a global spinner shown for any pending request in the app
- Says the layout is replaced by the loading UI too
- Thinks it only applies to the first server render, not navigation
- Expects it to display when a fetch rejects instead of the error UI
- Believes you must import and render it from page.tsx yourself