Skip to content

fix(react): render Stencil props as attributes during SSR - #850

Open
Burzmalian wants to merge 7 commits into
stenciljs:mainfrom
Burzmalian:fix/react-ssr-attributes
Open

Burzmalian wants to merge 7 commits into
stenciljs:mainfrom
Burzmalian:fix/react-ssr-attributes

Conversation

@Burzmalian

@Burzmalian Burzmalian commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Pull request checklist

  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been reviewed and added / updated if needed (for bug fixes / features)
  • Build (npm run build) was run locally for affected output targets
  • Tests (npm test) were run locally and passed
  • Prettier (npm run prettier) was run locally and passed

Pull request type

  • Bugfix
  • Feature
  • Code style update (formatting, renaming)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • Documentation content changes
  • Other (please describe):

What is the current behavior?

The client wrapper hands every Stencil prop to @lit/react, which sets it as a JS property after mount. Server HTML therefore has bare tags. For <MyButton variant="tertiary" size="lg">Open</MyButton> the server sends:

<my-button>Open</my-button>

The element upgrades with its default props and only gets the real ones when React hydrates, so every page flashes the wrong state first.

Issue URL: #848

What is the new behavior?

  • On the server, createComponent renders the tag directly with the Stencil props as attributes:
    <my-button variant="tertiary" size="lg">Open</my-button>
  • Attribute names come from the same properties map the server wrappers use, now also generated into the client wrappers, so @Prop({ attribute }) is honoured. Own accessors on the component prototype chain (above HTMLElement) tell Stencil props from pass-through props such as id or slot. Hand-written wrappers without the map fall back to the kebab-case name checked against static observedAttributes.
  • Non-primitive props stay client-only and false is omitted, same as the SSR wrapper in ssr.tsx.
  • suppressHydrationWarning is set on the element in both the server and the client render, so React does not report the server-only attributes.
  • Client behaviour is unchanged.
  • README: new "Server-side rendering" note. This covers host attributes only; shadow DOM and styles still need hydrateModule or @stencil/ssr.

Does this introduce a breaking change?

  • Yes
  • No

Server HTML gains attributes. Hydration was checked in Chrome with React 19.2: no warnings, attributes kept.

Other information

Tested in a Next.js 16 app with 107 components: no flash, no hydration errors, TTFB unchanged. Tests cover attribute mapping, omitted values, pass-through props and a class without observedAttributes.

@Burzmalian
Burzmalian requested a review from a team as a code owner September 28, 2026 20:58
@Burzmalian
Burzmalian force-pushed the fix/react-ssr-attributes branch 2 times, most recently from 585130a to 7b27800 Compare September 28, 2026 21:27

@davidpett davidpett left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great!

@Burzmalian
Burzmalian force-pushed the fix/react-ssr-attributes branch from 9178a8a to c491ee6 Compare September 29, 2026 14:46
@davidpett
davidpett enabled auto-merge (rebase) September 30, 2026 00:05
@Burzmalian Burzmalian closed this Oct 7, 2026
auto-merge was automatically disabled October 7, 2026 18:28

Pull request was closed

@Burzmalian Burzmalian reopened this Oct 7, 2026
@Burzmalian
Burzmalian force-pushed the fix/react-ssr-attributes branch 2 times, most recently from 5626c05 to 1262689 Compare October 13, 2026 18:33
Burzmalian and others added 7 commits October 22, 2026 12:14
The client wrapper set every Stencil prop as a JS property after mount,
so server HTML had bare tags and elements upgraded with default props
until React hydrated. On the server, render the tag with the props as
attributes, mapped through the element class (prototype accessors and
observedAttributes). Non-primitive props stay client-only and false is
omitted, matching the SSR wrapper. suppressHydrationWarning keeps React
from reporting the extra attributes.

Fixes stenciljs#848
… map

Generate the same prop-to-attribute map the server wrappers use into the client
wrappers and prefer it on the server, so @prop({ attribute }) names are honoured.
The observedAttributes lookup stays as a fallback for hand-written wrappers.
Document that this covers host attributes only.
`key in prototype` walks the whole chain, so under a server DOM shim inherited
props like id or title would be mistaken for Stencil props and dropped. Check own
accessors on the component prototype chain only, stopping at HTMLElement.
…its like Stencil

- Client wrapper mirrors the server's attribute set in a layout effect, so a prop set back to false or undefined removes its attribute; @lit/react only sets properties and Stencil does not reflect them
- Attribute fallback splits on digits (line1 -> line-1), matching Stencil's toDashCase
- Prop detection and attribute naming shared between the server and client paths
- jsdom spec: server HTML hydrates without warnings and attributes follow prop changes
- Keep suppressHydrationWarning on the client render: @lit/react's node build does not set it
The client layout effect updated or added an attribute for every Stencil prop, so
elements React created on the client gained host attributes they never had. Limit it
to attributes already present: server-rendered hosts stay in sync with their props,
client-rendered hosts stay property-only as before.
stenciljs#847 rewrote the generator with string templates; the client wrapper keeps its
properties map, placed after elementClass as before.
@Burzmalian
Burzmalian force-pushed the fix/react-ssr-attributes branch from 1262689 to b4271d9 Compare October 22, 2026 17:18
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.

2 participants