From f9853ae7034585383f961875a33eeee6f34d6e69 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 15:02:39 +0000 Subject: [PATCH] fix: detect destructive non-SSR mount and wrap it in error boundary 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 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 Claude-Session: https://claude.ai/code/session_01HADYAC46o2XCQPkCpavDyt --- packages/docs/src/pages/FAQ.mdx | 6 + packages/docs/src/pages/GettingStarted.mdx | 1 + packages/docs/src/pages/advanced/SSR.mdx | 4 + packages/docs/src/pages/learn/HowItWorks.mdx | 38 +++++++ .../e2e/fixture-multi-entry/src/entries.tsx | 5 + .../src/pages/Destructive.tsx | 8 ++ .../src/root-destructive.tsx | 21 ++++ .../static/e2e/tests-dev/multi-entry.spec.ts | 42 +++++++ packages/static/e2e/tests/multi-entry.spec.ts | 41 +++++++ .../skills/funstack-static-knowledge/SKILL.md | 2 + packages/static/src/client/entry.tsx | 24 +++- packages/static/src/client/error-boundary.tsx | 84 +++++++++----- .../src/client/mount-validation.test.ts | 107 ++++++++++++++++++ .../static/src/client/mount-validation.ts | 91 +++++++++++++++ packages/static/src/rsc/marker.ts | 4 +- 15 files changed, 444 insertions(+), 34 deletions(-) create mode 100644 packages/static/e2e/fixture-multi-entry/src/pages/Destructive.tsx create mode 100644 packages/static/e2e/fixture-multi-entry/src/root-destructive.tsx create mode 100644 packages/static/src/client/mount-validation.test.ts create mode 100644 packages/static/src/client/mount-validation.ts diff --git a/packages/docs/src/pages/FAQ.mdx b/packages/docs/src/pages/FAQ.mdx index 4703420..c12cc51 100644 --- a/packages/docs/src/pages/FAQ.mdx +++ b/packages/docs/src/pages/FAQ.mdx @@ -1,5 +1,11 @@ # FAQ +## I'm seeing a ``The Root component renders other content next to `{children}` `` error + +With `ssr: false` (the default), the App is mounted into the parent element of `{children}` in your Root component, and React removes all other content from that element when mounting. This error means your Root component renders content that is silently removed this way in production builds. + +Make `{children}` the only content of its parent element in the Root component (for example, wrap it in a dedicated `
`). See [How It Works](/learn/how-it-works#keep-children-alone-in-its-parent-element) for details. + ## I'm seeing a `` must have a valid `as` value`` warning This is a bug in React itself. Please wait for [the fix](https://github.com/facebook/react/pull/34760) to be released. diff --git a/packages/docs/src/pages/GettingStarted.mdx b/packages/docs/src/pages/GettingStarted.mdx index 927cd01..fbf0704 100644 --- a/packages/docs/src/pages/GettingStarted.mdx +++ b/packages/docs/src/pages/GettingStarted.mdx @@ -65,6 +65,7 @@ The Root component: - is responsible for defining the shell HTML structure of your app - is a server component - **CANNOT** import client components; you could, but they are fully rendered into static HTML and never hydrated +- must render `{children}` as the only content of its parent element (unless SSR is enabled); see [How It Works](/learn/how-it-works#keep-children-alone-in-its-parent-element) ### 3. Create Your App Component diff --git a/packages/docs/src/pages/advanced/SSR.mdx b/packages/docs/src/pages/advanced/SSR.mdx index d845e13..5f00eb6 100644 --- a/packages/docs/src/pages/advanced/SSR.mdx +++ b/packages/docs/src/pages/advanced/SSR.mdx @@ -30,6 +30,10 @@ This improves perceived performance, especially on: The browser can start painting content as soon as the HTML arrives, while JavaScript loads in the background. Once loaded, React hydrates the existing HTML to make it interactive. +### No Root Layout Constraint + +Without SSR, `{children}` must be the only content of its parent element in the Root component ([details](/learn/how-it-works#keep-children-alone-in-its-parent-element)). With SSR enabled, the whole document is hydrated instead of mounting the App into a container element, so this constraint does not apply. + ## Cons ### Client Components Must Be SSR-Capable diff --git a/packages/docs/src/pages/learn/HowItWorks.mdx b/packages/docs/src/pages/learn/HowItWorks.mdx index 4364764..fa51da8 100644 --- a/packages/docs/src/pages/learn/HowItWorks.mdx +++ b/packages/docs/src/pages/learn/HowItWorks.mdx @@ -85,6 +85,44 @@ The Root component is special in two ways: The Root entrypoint is a FUNSTACK Static counterpart to the `index.html` file in traditional SPAs. It allows you to still leverage some of the benefits of server components for defining the HTML shell of your application. +### Keep `{children}` Alone in Its Parent Element + +With `ssr: false` (the default), the App is mounted on the client into the parent element of `{children}`. React clears all existing content of that element when mounting, so any other content the Root component renders **in the same element** is removed from the page the moment the App mounts: + +```tsx +// ❌ BAD:
and