next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Suspense boundary broken (ignored) after second server action call

Open tilman opened this issue 1 year ago • 9 comments

Link to the code that reproduces this issue

https://github.com/trieb-work/nextjs-broken-suspense-bug-example

To Reproduce

  • start the app in dev mode using pnpm dev and open it in the browser
  • While this page loads you should see first 'page is loading...' from the loading.tsx
  • Then page.tsx returns from it's mocked api calls after 1s and is displaying 'Slept for 1000ms. Random digit X'
  • Then SlowServerComponent inside of a suspense in page.tsx returns from it's mocked api calls after 3s and is displaying 'Slept for 3000ms. Random digit X'
  • So far so good, everything of suspense logic works as expected so far
  • Now run the server action on the page via the button "run server action". The behavior should be the same as previously in (no loading.tsx is expected but suspense should work)
  • Now run the server action again and watch how "Slept for 1000ms. Random digit XX" and "Slept for 3000ms. Random digit XX" are displayed at the same time (after 4000ms, after both (1000ms + 3000ms) have returned).
  • The suspense is now "ignored". "Slept for 1000ms. Random digit XX" should show up before "Slept for 3000ms. Random digit XX" because of the suspense boundary.
  • But it seems like the suspense boundary get's ignored if the page is refreshed via an server action for the second time.
  • This bug results in bad user experience because long running API calls will block the whole page update and not only a small portion of the UI.
  • This behavior now persists for all upcoming form action calls until the page is hard reloaded via the browser navigation.

If starting the app in production mode (pnpm build + pnpm start) the same problem persists. Only difference will be that there is no loading page displayed first (due to static optimization). But this can also be disabled if page is opting out of static optimization.

Current vs. Expected behavior

Current:

  • after the second use of the form server action the suspense boundary is ignored and all server components are returned at once after the last server component has finished rendering.

Expected:

  • same behaviour as for initial render or for first server action call
  • in particular: the suspense should always behave the same

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.6.0: Wed Jul  5 22:22:05 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T6000
  Available memory (MB): 32768
  Available CPU cores: 10
Binaries:
  Node: 18.19.0
  npm: 10.2.3
  Yarn: 1.22.21
  pnpm: 8.6.0
Relevant Packages:
  next: 15.0.0-canary.3 // Latest available version is detected (15.0.0-canary.3).
  eslint-config-next: N/A
  react: 19.0.0-rc-6f23540c7d-20240528
  react-dom: 19.0.0-rc-6f23540c7d-20240528
  typescript: 5.3.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Navigation

Which stage(s) are affected? (Select all that apply)

next dev (local), next start (local), Vercel (Deployed)

Additional context

No response

tilman avatar May 31 '24 21:05 tilman

I'm having similar issues.

  • loading.tsx is triggered on page load. (Expected)
  • If I invoke an action with revalidatePath("/") on /, loading.tsx is not triggered (expected?)
  • If I invoke an action with revalidatePath("/other-page") on /other-page, loading.tsx is triggered only on the first invocation, but not for subsequent invocations (not expected?)

This works as expected for me up to v14.2.0-canary.23, in v14.2.0-canary.24 I start to see issues.

kevinmitch14 avatar Jun 03 '24 14:06 kevinmitch14

@kevinmitch14 I think this is a different issue. My Problem is regarding Suspense boundaries and not regarding loading.tsx. I suggest you open a new issue or start a discussion on this topic.

tilman avatar Jun 03 '24 14:06 tilman

loading.tsx is a Suspense boundary.

https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#instant-loading-states

When did the issues start to occur for you? Have you tried something before v14.2.0-canary.24?

kevinmitch14 avatar Jun 03 '24 14:06 kevinmitch14

Jep, I have tested and it was present in v14.2.4 as well

tilman avatar Jun 07 '24 11:06 tilman

I have noticed it couple of month ago but never understood what is happening because of the hard way to reproduce it. But with the github example I linked above you can go back to older nextjs versions and reproduce it.

tilman avatar Jun 07 '24 11:06 tilman

A temporal workaround is to add a key to the suspense boundary which changes on every render. But this feels kinda strange and I'm unsure about the negative consequences it has:

<Suspense key={Math.random()} fallback={...}>
   ...
<Suspense/>

tilman avatar Jun 07 '24 11:06 tilman

v15.0.0-canary.14 has fixed a similar issue I was having from 14.2.2.

luke-concannon avatar Jun 07 '24 22:06 luke-concannon

Hey Luke, thanks for the hint. I have upgraded the example to 15.0.0-canary.23. Now the bug is even worse. Even on the first try it is not working now. In 15.0.0-canary.3 it was at least working for the first call of a server action and then the bug occured on all upcomming calls.

tilman avatar Jun 10 '24 13:06 tilman

This issue has been automatically marked as stale due to inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you.

nextjs-bot avatar Dec 10 '25 23:12 nextjs-bot