Open MinnDevelopment opened 1 month ago
Consider a file structure like this in Next.js:
- loading.tsx
- /(foo)
- /a
- page.tsx
- /b
- page.tsx
- /(bar)
- /c
- page.tsx
- /d
- page.tsx
In this setup, Next.js creates component trees for each route group. So for (foo), the component tree looks like this:
<TemplateContext key="(foo)">
<Suspense fallback={<LoadingTSX />}>
{/* content of /a or /b */}
</Suspense>
</TemplateContext>
And similarly for (bar):
<TemplateContext key="(bar)">
<Suspense fallback={<LoadingTSX />}>
{/* content of /c or /d */}
</Suspense>
</TemplateContext>
When you navigate within the same route group (e.g., from /a to /b), Next.js updates the content:
<TemplateContext key="(foo)">
<Suspense fallback={<LoadingTSX />}>
<A /> {/* previous page */}
</Suspense>
</TemplateContext>
to
<TemplateContext key="(foo)">
<Suspense fallback={<LoadingTSX />}>
<B /> {/* next page */}
</Suspense>
</TemplateContext>
Even if <B />
takes time to load, the <Suspense>
fallback (loading state) doesn’t appear (even if the streaming brings in the loading state at the start of the connection), because React does not re-show the fallback in a transition. This behavior is explained here in React’s documentation.
However, when you navigate across different route groups (e.g., from (foo) to (bar)), Next.js changes the key:
<TemplateContext key="(foo)">
<Suspense fallback={<LoadingTSX />}>
<A />
</Suspense>
</TemplateContext>
to
<TemplateContext key="(bar)">
<Suspense fallback={<LoadingTSX />}>
<C />
</Suspense>
</TemplateContext>
Since the key is different, React remounts the component tree, triggering the <Suspense>
fallback. This means the loading state will display at the start of the transition.
On the other hand, for a file structure like this
- loading.tsx
- /a
- page.tsx
- /b
- page.tsx
- /c
- page.tsx
- /d
- page.tsx
When you navigate (e.g., from /a to /b), Next.js updates the content:
<TemplateContext key="a">
<Suspense fallback={<LoadingTSX />}>
<A /> {/* previous page */}
</Suspense>
</TemplateContext>
to
<TemplateContext key="b">
<Suspense fallback={<LoadingTSX />}>
<B /> {/* next page */}
</Suspense>
</TemplateContext>
Due to the key change, loading state will show up.
In general, to determine if loading.tsx
will show up or not, you need to check if the navigation changes the segment which loading.tsx
applies to. In the first example, loading.tsx
applies to segment (foo)
and (bar)
, so you need a navigation like /a
to /c
to to trigger loading. Nav from /a
to /b
won't work because the segment which loading.tsx
applies to does not change, both are (foo)
. In the second example, because loading.tsx
applies to segment /a
, /b
, /c
, /d/
, hence the loading would show up when you nav from /a
to /c
.
Link to the code that reproduces this issue
GitHub repo CodeSandbox
To Reproduce
app/(foo)
andapp/(bar)
and aloading.tsx
in the top level atapp/loading.tsx
.(foo)/a
->(foo)/b
for instance usingnext/link
Current vs. Expected behavior
The loading should show for these navigations, since a loading.tsx is present at the root. Instead, it only works for navigation between route groups but not in the same group.
Provide environment information
Which area(s) are affected? (Select all that apply)
Navigation
Which stage(s) are affected? (Select all that apply)
next start (local), Other (Deployed)
Additional context
Copying the
loading.tsx
into every folder solves the problem, but that doesn't seem like the correct behavior to me, since the documentation suggests that it applies to all children.Navigating from a page in
(foo)
to a page in(bar)
works as expected and shows the loading screen. It only breaks for navigation within the same group.