React 19 support for web components

Historically React was not a super friendly framework for web-components. The current major brings a lot of improvements including 100% score on custom elements.
I have prepared a little playground with the both:

  • react 18 CSR and SSR examples
  • react 19 CSR and SSR examples

SSR is based on the React Router 7. Available on Github

Note: The implementation of web-component in the repository is just an example. It is not optimized at all.

Before React 19 era

Previous versions of React had issues to support the web-components fully.

Unknown props treated as HTML attributes

There is a difference between:

  • HTML attributes
  • HTML properties

When you use the props of the web-component to pass some object, React will treat it as attribute - it will become a string:

<my-web-component theme={{ primary: 'red', secondary: 'green' }}>

In this case, React would attempt to set the theme as an HTML attribute, resulting in:

<my-web-component theme="[object Object]">

The workaround for it was to create a ref and set it when the component was mounted

const webComponentRef = useRef(null);

useEffect(() => {
  if (webComponentRef.current) {
    webComponentRef.current.theme = { primary: 'red', secondary: 'green' };
  }
}, []);

return <my-web-component ref={webComponentRef} />;

Event handling

React's synthetic event system couldn't properly handle custom events emitted by web components. You couldn't use the declarative event handling:

<my-web-component onCustomEvent={handleCustomEvent} />

Like in the previous problem, there is a workaround with using ref and effect in which you can add event listeners manually:

const webComponentRef = useRef(null);

useEffect(() => {
  const element = webComponentRef.current;
  if (element) {
    element.addEventListener('custom-event', handleCustomEvent);
    return () => element.removeEventListener('custom-event', handleCustomEvent);
  }
}, []);

return <my-web-component ref={webComponentRef} />

SSR issues

Server-side rendering was problematic due to the:

  • potential mismatches between server-rendered content and client hydration
  • shadow dom serialization
// Server-side rendering
const App = () => <my-web-component initialData={{foo: 'bar'}} />;

// This would render as:
<my-web-component initialdata="[object Object]"></my-web-component>

// This could cause hydration mismatches and incorrect initialization

I have added the examples of hydration mismatches in SSR and other problems in the repository

In React 19 era

The ref-based workaround is no longer needed. You can add callbacks and props like you would normally do in React components.
You can check the full React 19 CSR example here

Props improvements

Props that match a property on the Custom Element instance will be assigned as properties.
We no longer need a ref workaround.

// will work as expected
<my-web-component theme={{ primary: 'red', secondary: 'green' }}>

Example with props

Event handling improvements

React will now understand every event you emit by adding onEventName callback to the web-component

// web-component emits custom event PascalCase

new CustomEvent('NameChanged');

<my-web-component onNameChanged={handleNameChanged}></my-web-component>

SSR improvements

React 19 eliminates the need for many workarounds previously necessary to integrate web components into React.

  • Props passed to a custom element are rendered as attributes if their type is a primitive value (such as a string, number, or the boolean value true)
  • Props with non-primitive types (such as objects, symbols, functions, or false) are omitted, then applied on the client if the custom element instance has a matching property,

You can read more about it in the source

You can check how the example integration works here react-19-ssr-example

Summary

React 19 brings long awaited support for the web-components to treat them as first-class citizens.
However preparing the web-component for proper SSR support may not be so easy. If the web-component is not prepared for the SSR on the implementation level, you can leverage:

  • use-client directive if its supported by your framework
  • dynamic import in useEffect which will run only on the client and register it there

In case you are interested in SSR and web-components, this is great resource provides deeper insights.

In short, use React 19 and you are fine with client side rendering. For SSR do the research first if you can integrate it smoothly to your framework.