vercel / next.js

The React Framework
https://nextjs.org
MIT License
126.72k stars 26.94k forks source link

App router loading.tsx does not work as expected across routing groups #69625

Open MinnDevelopment opened 1 month ago

MinnDevelopment commented 1 month ago

Link to the code that reproduces this issue

GitHub repo CodeSandbox

To Reproduce

  1. Create a project with 2 route groups app/(foo) and app/(bar) and a loading.tsx in the top level at app/loading.tsx.
  2. Put 2 pages in each group that have a simple suspending hook like a 2s timeout
  3. Try to navigate between pages in the same group (foo)/a -> (foo)/b for instance using next/link
  4. Loading does not show, instead the router just waits until the suspense finishes.

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

Operating System:
  Platform: linux
  Arch: x64
  Version: #1 SMP PREEMPT_DYNAMIC Sun Aug  6 20:05:33 UTC 2023
  Available memory (MB): 4102
  Available CPU cores: 2
Binaries:
  Node: 20.9.0
  npm: 9.8.1
  Yarn: 1.22.19
  pnpm: 8.10.2
Relevant Packages:
  next: 15.0.0-canary.134 // There is a newer canary version (15.0.0-canary.139) available, please upgrade! 
  eslint-config-next: N/A
  react: 19.0.0-rc-7771d3a7-20240827
  react-dom: 19.0.0-rc-7771d3a7-20240827
  typescript: 5.3.3

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.

gaojude commented 23 hours 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.