Skip to content

fix(react-tunnel): fill the exit before paint when it mounts in a Suspense reveal - #3314

Open
mfal wants to merge 1 commit into
mainfrom
fix/react-tunnel-exit-suspense-reveal
Open

mfal wants to merge 1 commit into
mainfrom
fix/react-tunnel-exit-suspense-reveal

Conversation

@mfal

@mfal mfal commented Sep 28, 2026

Copy link
Copy Markdown
Member

What & why

A TunnelExit that mounts in a Suspense reveal stays empty for one painted frame. In mStudio this makes every row of a List whose items load individually jump: a ListItemView renders for one frame without its tunnelled Avatar, Heading and Text (36 px instead of 61 px), then snaps back.

Cause:

  • ListItemView renders its exits before the tunnelled children, so the exit's first render finds no entries.
  • The entries commit their children in layout effects. The exit is a MobX observer, and it subscribes via useSyncExternalStore in a passive effect.
  • For a sync mount, React flushes passive effects before paint, so nothing is visible. For a Suspense reveal or a transition, it runs them after paint.

Fix: until the exit has rendered committed entries, it dispatches a re-render from a layout effect. React processes that update after every layout effect of the commit and before paint. Cost: one extra render per exit mount while it has no committed entries.

Tests

  • New: "Content from entry is in the DOM when the exit mounts in a Suspense reveal" reads the exit in the first MutationObserver callback after the reveal commit. It is red on main ('') and green with the fix.
  • Changed: "Content is not rendered if removing previously suspended tunnel entry" advanced the fake timers once. That relied on React pre-rendering the suspended siblings (and starting Lazy500's timer) before the advance. The extra sync render moves the pre-render to a later task, so the test now advances in vitest.waitFor until the entries are there. The assertions are unchanged.
  • WebKit: the react-tunnel suite passed 5 of 5 runs (27 tests). The List, Label, Heading and Tabs browser tests in components pass (59 tests).
  • Firefox not run locally; it fails to launch in my sandbox.
  • mStudio members list, with this build dropped into node_modules: 0 empty rows in 2 runs, down from all 20 rows for at least one painted frame.

Checklist

  • PR title is a Conventional Commit and matches the base branch above
  • pnpm lint is clean (pre-push hook); pnpm affected:test not run in full — see Tests
  • Generated code is committed — none affected
  • User-facing strings — none
  • Docs — no public API change

🤖 Generated with Claude Code

…pense reveal

An exit that mounts together with entries rendered after it (as in
`ListItemView`) shows its entries only once its observer subscribes, in a
passive effect. React runs the passive effects of a Suspense reveal or a
transition after paint, so the browser painted one frame with an empty exit.
The exit now re-renders from a layout effect until it has rendered committed
entries; that update runs before paint.

The "previously suspended tunnel entry" test advanced the fake timers once and
relied on React pre-rendering the suspended siblings before that. The extra
sync render moves the pre-render to a later task, so the test now advances
until the entries are there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for ./packages/components/

Status Category Percentage Covered / Total
🔵 Lines 75.25% 1180 / 1568
🔵 Statements 74.81% 1209 / 1616
🔵 Functions 69.94% 270 / 386
🔵 Branches 69.4% 617 / 889
File CoverageNo changed files found.
Generated in workflow #7024 for commit 999728a by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

Preview environments are ready:

Type URL
docs pr-3314.docs.review.flow-components.de
storybook pr-3314.storybook.review.flow-components.de

Images:

  • docs: ghcr.io/mittwald/flow/docs:pr-3314
  • storybook: ghcr.io/mittwald/flow/storybook:pr-3314

@mfal
mfal marked this pull request as ready for review September 28, 2026 15:51
@mfal
mfal requested a review from a team September 28, 2026 15:51

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant