How to Fix: loading.tsx Component not rendering, instead rendering something basic
Your loading.tsx is usually not broken when Next.js shows a plain fallback instead of your custom UI. In most cases, the framework is rendering a different route segment boundary, skipping the file because of placement, or resolving the route too fast for the intended Suspense loading state to appear.
Table of Contents
Understanding the Root Cause
In the Next.js App Router, loading.tsx is tied to a specific route segment. It only renders when that segment is waiting on async work. If the file is placed in the wrong folder, if a parent layout is controlling the transition, or if data is already available immediately, Next.js may render a simpler fallback or move directly to the final page.
This behavior commonly happens for one of these reasons:
- Wrong file location: loading.tsx must live inside the exact route segment where the loading UI should appear.
- App Router vs Pages Router mismatch: loading.tsx works in the app directory, not the legacy pages directory.
- No async boundary: if the page or layout resolves synchronously, the loading state may never become visible.
- Parent segment interception: a parent layout.tsx or nested route can determine which loading boundary displays.
- Client component assumptions: marking components with ‘use client’ does not make loading.tsx behave like a manual spinner. It still follows route-level Suspense rules.
- Streaming behavior: Next.js may stream server-rendered output in a way that makes a very brief loading state appear like a generic placeholder.
If the reproduction is private, the fastest way to debug is to verify the routing structure first, because most loading.tsx not rendering issues come from file placement rather than rendering logic.
Step-by-Step Solution
Use this checklist to make sure Next.js recognizes and renders the correct loading boundary.
1. Confirm you are using the App Router
Your route should be structured under the app directory.
app/
dashboard/
loading.tsx
page.tsx
Correct example:
// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading dashboard...</div>
}
// app/dashboard/page.tsx
async function getData() {
const res = await fetch('https://example.com/api/data', {
cache: 'no-store'
})
return res.json()
}
export default async function DashboardPage() {
const data = await getData()
return <div>{data.title}</div>
}
If your route lives under pages/, loading.tsx will not work as expected.
2. Place loading.tsx in the exact segment that should suspend
If the issue occurs on a nested route, the file must exist in the matching folder.
app/
products/
loading.tsx
page.tsx
[id]/
loading.tsx
page.tsx
In this structure:
- app/products/loading.tsx handles the /products segment.
- app/products/[id]/loading.tsx handles the /products/123 detail segment.
If you expect a detail page loading state but only define the file in the parent segment, Next.js may render something different from what you expect.
3. Ensure the page actually performs async work
loading.tsx only appears when the route is waiting. If your page is static or fully cached, the loading UI may never show.
// app/profile/page.tsx
async function getProfile() {
const res = await fetch('https://example.com/api/profile', {
cache: 'no-store'
})
return res.json()
}
export default async function ProfilePage() {
const profile = await getProfile()
return <section>{profile.name}</section>
}
For debugging, forcing dynamic behavior helps confirm the boundary works:
// app/profile/page.tsx
export const dynamic = 'force-dynamic'
async function getProfile() {
const res = await fetch('https://example.com/api/profile', {
cache: 'no-store'
})
return res.json()
}
export default async function ProfilePage() {
const profile = await getProfile()
return <section>{profile.name}</section>
}
4. Verify parent layout behavior
A layout.tsx higher in the tree can affect what users see during navigation. Make sure the loading file exists at the right level relative to the layout.
app/
layout.tsx
loading.tsx
settings/
layout.tsx
loading.tsx
page.tsx
If the settings segment has its own async boundary, use app/settings/loading.tsx. Otherwise, the root loading UI may be shown instead.
5. Keep loading.tsx simple and server-safe
A route loading file should be minimal and should not depend on browser-only APIs unless you intentionally convert its children into client components.
// app/orders/loading.tsx
export default function Loading() {
return (
<div aria-busy="true" aria-live="polite">
<p>Loading orders...</p>
</div>
)
}
Avoid adding logic that can throw during the loading phase. If the loading component errors, Next.js can fall back to a more basic render path that looks unrelated to your custom UI.
6. Test through real route navigation
Some developers expect loading.tsx to appear on the initial instant render in development, but it is most noticeable during client-side navigation between routes.
// example navigation
import Link from 'next/link'
export default function Home() {
return (
<nav>
<Link href="/dashboard">Open dashboard</Link>
</nav>
)
}
Navigate from one route to another and watch whether the segment-level loading UI appears. If you hard refresh directly onto a fast route, the fallback may be too brief to notice.
7. Check for conflicting route features
If you use parallel routes, intercepted routes, or nested Suspense boundaries, a different loading state may be winning. Simplify the route temporarily:
app/
test-route/
loading.tsx
page.tsx
Then create a minimal async page:
// app/test-route/page.tsx
async function wait() {
await new Promise((resolve) => setTimeout(resolve, 2000))
}
export default async function TestRoutePage() {
await wait()
return <div>Loaded</div>
}
If this works, the issue is not with loading.tsx itself but with your route architecture.
8. Production-build test
Development mode can behave differently because of fast refresh, caching differences, and debug overlays. Always confirm with a production build.
npm run build
npm run start
If the loading UI appears correctly in production, the issue may be limited to development-only behavior.
Common Edge Cases
- loading.tsx inside the wrong directory: placing it next to reusable components instead of the active route segment prevents Next.js from picking it up.
- Static rendering: when data is pre-rendered or cached aggressively, there is no visible wait state.
- Instant fetch resolution: very fast APIs can make the fallback flash too quickly to notice.
- Nested layouts: a parent or sibling segment may display its own fallback instead of the one you intended.
- Using the Pages Router: pages/ does not support App Router loading boundaries.
- Errors during loading render: if your loading component imports incompatible code, the framework may show a basic fallback or error boundary.
- Client-side state expectations: loading.tsx is not a replacement for local component loading state triggered by button clicks or client fetches after mount.
- Route groups: folders like (marketing) do not affect the URL, but they do affect where route files belong in the segment tree.
FAQ
Why does my loading.tsx never appear even though the page fetches data?
The most common reason is that the route is being statically rendered or the data resolves too quickly. Force dynamic rendering temporarily with export const dynamic = ‘force-dynamic’ and use cache: ‘no-store’ on the fetch to test the boundary.
Why am I seeing a generic loading UI instead of my custom component?
This usually means a different route segment or parent layout owns the active loading boundary, or your custom loading.tsx is not in the correct folder. Verify the exact path for the route being navigated.
Does loading.tsx work for client-side fetching inside a component?
Not by itself. loading.tsx is for route-level loading in the App Router. If a client component fetches data after mount, you still need local state, a component-level Suspense setup, or a dedicated skeleton component.
The practical fix is to treat this bug as a route segment resolution problem. Confirm the file is inside the correct app path, ensure the route actually suspends, test a minimal delayed page, and then rebuild complexity step by step. That approach isolates why Next.js is rendering a basic fallback instead of your intended loading.tsx component.