Next.js App Router Why doesn’t my “loading” state show up when fetching details for a parallel route
Unanswered
Golden paper wasp posted this in #help-forum
Golden paper waspOP
I have a Next.js (v13+/15) App Router setup for a “master-detail” style page. My folder structure is something like this:
ListOfThingsView is a client component. When a user clicks on one of these items, I want to show its details in the same screen, using a “details” parallel route. So in that client component, I do something like:
My layout.tsx in list-of-things is set up with parallel routes:
And in @details/page.tsx, I do another fetch to get the details based on searchParams.id:
I also have a loading.tsx in list-of-things/@details/ (and one in list-of-things/ too) in hopes that while the details are fetching, some loading indicator will appear. But in practice, I often don’t see the loading state at all. Sometimes it flashes briefly, sometimes not at all.
Why isn’t the loading.tsx (or a fallback) consistently showing up when I navigate between different id values (e.g. changing the query from id=abc to id=xyz)?
Is there a “best practice” for forcing Next.js to show a loading or suspense state while the details are being fetched in a parallel route?
Am I missing something about how query param changes vs. route segment changes affect the loading states?
Things I’ve tried:
Putting
Using router.push vs router.replace.
Setting cache: 'no-store' on the fetch.
app/
└─ list-of-things/
├─ page.tsx
├─ layout.tsx
├─ loading.tsx
└─ @details/
├─ page.tsx
├─ loading.tsx// app/list-of-things/page.tsx (Server Component)
export default async function ListOfThingsPage() {
const listOfThings = await fetch(/* ... */).then((res) => res.json());
return (
<ListOfThingsView listOfThings={listOfThings} />
);
}ListOfThingsView is a client component. When a user clicks on one of these items, I want to show its details in the same screen, using a “details” parallel route. So in that client component, I do something like:
// components/ListOfThingsView.tsx (Client Component)
'use client';
import { useRouter } from 'next/navigation';
export default function ListOfThingsView({ listOfThings }) {
const router = useRouter();
const handleClick = (id: string) => {
// I want to load the details in the @details slot
router.replace(`/list-of-things?id=${id}`, { scroll: false });
};
return (
<div>
{listOfThings.map((thing) => (
<button key={thing.id} onClick={() => handleClick(thing.id)}>
{thing.name}
</button>
))}
</div>
);
}My layout.tsx in list-of-things is set up with parallel routes:
// app/list-of-things/layout.tsx
export default function Layout({
children,
details,
}: {
children: React.ReactNode;
details: React.ReactNode;
}) {
return (
<div style={{ display: 'flex' }}>
<div style={{ flex: 1 }}>{children}</div>
<div style={{ flex: 1 }}>{details}</div>
</div>
);
}And in @details/page.tsx, I do another fetch to get the details based on searchParams.id:
// app/list-of-things/@details/page.tsx
export default async function DetailsPage({
searchParams,
}: {
searchParams: Promise<{ id?: string }>;
}) {
const {id} = await searchParams;
if (!id) {
return <div>No item selected.</div>;
}
const details = await fetch(/* ... */).then((res) => res.json());
return <DetailsView details={details} />;
}I also have a loading.tsx in list-of-things/@details/ (and one in list-of-things/ too) in hopes that while the details are fetching, some loading indicator will appear. But in practice, I often don’t see the loading state at all. Sometimes it flashes briefly, sometimes not at all.
Why isn’t the loading.tsx (or a fallback) consistently showing up when I navigate between different id values (e.g. changing the query from id=abc to id=xyz)?
Is there a “best practice” for forcing Next.js to show a loading or suspense state while the details are being fetched in a parallel route?
Am I missing something about how query param changes vs. route segment changes affect the loading states?
Things I’ve tried:
Putting
<Suspense/> around the details slot in the layout with a fallback.Using router.push vs router.replace.
Setting cache: 'no-store' on the fetch.