Skip to content

fix: detect destructive non-SSR mount and wrap it in error boundary#158

Merged
uhyo merged 1 commit into
masterfrom
claude/destructive-mount-issue-5s0vea
Jul 20, 2026
Merged

fix: detect destructive non-SSR mount and wrap it in error boundary#158
uhyo merged 1 commit into
masterfrom
claude/destructive-mount-issue-5s0vea

Conversation

@uhyo

@uhyo uhyo commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Closes #143

Problem

With ssr: false, the App is mounted into the parent element of {children} in Root, and React clears all other content of that element on the first render. Content the Root rendered next to {children} was silently wiped in production builds while surviving in dev (where the full tree is rendered by React). Additionally, the prod non-SSR mount path was the only one of the four mount paths without GlobalErrorBoundary and StrictMode.

Per discussion, this PR takes the documentation + runtime validation approach rather than introducing a framework-owned wrapper element.

Changes

Runtime validation (src/client/mount-validation.ts)

  • Before mounting, the marker's container is inspected and a console.error is reported listing the content that the mount destroys, with a link to the docs. The mount still proceeds (warning, not a hard failure, since the check runs in end users' browsers).
  • The check also runs in dev: the served shell HTML has the same shape as the production build (marker span inside its container), so it is validated before createRoot(document) replaces the document. The marker is located by its id prefix, so no manifest plumbing is needed. This surfaces the problem on every dev page load, closing the dev/prod divergence.
  • Whitespace-only text, comments, and <script> elements are exempt — the framework itself emits bootstrap/RSC-payload scripts next to the marker when Root uses <body>{children}</body>.
  • The core logic is DOM-agnostic (structural types) and unit-tested without a DOM implementation.

Error boundary / StrictMode

  • The prod non-SSR mount is now wrapped in <StrictMode><GlobalErrorBoundary> like the other three paths.
  • GlobalErrorBoundary gained an embedded prop: the existing fallback renders a full <html><body> document, which would be invalid when mounted into an inner container element, so the embedded variant renders the same content in a <div> instead.

Docs

  • New "Keep {children} Alone in Its Parent Element" section in How It Works (linked from the console error), a Getting Started bullet, a "No Root Layout Constraint" pro on the SSR page, a FAQ entry keyed to the error message, and a note in the bundled knowledge skill.
  • All pre-existing Root examples in the docs already comply.

Tests

  • 10 new unit tests for the validation logic.
  • New destructive.html entry in the multi-entry e2e fixture with a violating Root:
    • prod suite asserts the sibling content is actually destroyed and the warning fires;
    • dev suite asserts the content survives but the warning still fires;
    • both assert compliant Roots produce no warning.

Verification

  • pnpm typecheck, pnpm lint, pnpm format:check clean
  • Unit tests: 91 passed
  • E2E: 31/31 (build/preview) and 28/28 (dev server) passed

🤖 Generated with Claude Code

https://claude.ai/code/session_01HADYAC46o2XCQPkCpavDyt


Generated by Claude Code

With `ssr: false`, the App is mounted into the parent element of
`{children}` in Root, and React clears all other content of that element
on the first render. Content the Root rendered next to `{children}` was
silently wiped in production builds while surviving in dev, where the
full tree is rendered by React (#143).

- Report a console error listing the content that the mount destroys.
  The check also runs in dev by inspecting the served shell HTML (which
  has the same shape as the production build) before React replaces the
  document, so the problem is surfaced on every dev page load too.
  Whitespace, comments, and framework-emitted script elements are
  exempt.
- Wrap the prod non-SSR mount in StrictMode and GlobalErrorBoundary
  like the other three mount paths, using a new embedded fallback since
  the mount point is an inner element where the document-level
  fallback's <html> would be invalid.
- Document the "keep {children} alone in its parent element" constraint
  in How It Works, Getting Started, SSR docs, FAQ, and the bundled
  knowledge skill.

Closes #143

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HADYAC46o2XCQPkCpavDyt
@uhyo
uhyo merged commit 7be9935 into master Jul 20, 2026
2 checks passed
@uhyo
uhyo deleted the claude/destructive-mount-issue-5s0vea branch July 20, 2026 15:08
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.

Prod non-SSR client mount: no error boundary, and mounting clears sibling DOM in the container

2 participants