Next.js Discord

Discord Forum

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
Open in Discord
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:
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.

0 Replies