React 19 introduces full support for custom elements

December 6, 2024

React 19 has just been released on 5 December 2024 and brings with it many new features and improvements, each of which is very exciting, but I’d like to focus your attention on one of them:

Support for Custom Elements

React 19 adds full support for custom elements and passes all tests on

Custom Elements Everywhere

source: https://react.dev/blog/2024/12/05/react-19

It’s no secret that Web Components support in React has been a long-anticipated feature. Issue #11347 about properties/attributes support for custom elements in React was created in October 2017 (seven years ago!), but the React team explained that they didn’t want to rush and make quick decisions that they would regret later.

We’d be happy to support WCs better in React. We don’t want to rush it, and want to make sure this support is well thought-out.

source: https://dev.to/dan_abramov/comment/6kdc

Component libraries using Web Components have needed to create special React wrappers due to integration challenges between the technologies. For example, Microsoft created @microsoft/fast-react-wrapper for their FAST component library, while Adobe developed wrappers for their Adobe Spectrum library under the @swc-react scope.

But finally, thanks for efforts from josepharhar for driving design and implementation, React developers can work with Web Components without any wrappers.

What was not great with Web Components in React 18?

The React team briefly mentioned that previously React treated all unknown properties as attributes. This created a significant issue because HTML attributes can only have string values. To pass an object, developers had to either use JSON.stringify()/JSON.parse() for data conversion or create a Ref to access the DOM node directly in an imperative style. Most developers opted for the Ref approach, but this wasn’t ideal — it deviated from React’s declarative paradigm and required extra boilerplate code. Unfortunately, using Refs was the only way to work with component properties.

📜 Note

If you don’t know what’s the difference between attribute and property, I highly recommend this very good article from Jake Archibald “HTML attributes vs DOM properties

The same applies to CustomEvent support. To attach an event listener of a CustomEvent, you must create a Ref to the web component and interact with the DOM node directly using addEventListener() and removeEventListener().

Finally, there’s the matter of how boolean attributes work in HTML. The presence of an attribute means true, while its absence means false. Before React 19, this behavior only worked for standard attributes like disabled. For custom boolean attributes, developers had to create a separate props object, add the attributes that should be true, and then spread the result object.

It’s understandable why library authors chose to encapsulate all this complexity and boilerplate code within wrappers.

Code examples

💻 You can try all the code here: https://stackblitz.com/~/github.com/aleks-elkin/react-web-components

Fortunately, React 19 resolves all these issues. Let’s create a two React apps with React 18 and React 19 that uses the same web component, web-counter, and compare.

web-counter is a simple component, that has an increment button and displays a current value. It has:

  • One boolean attribute, isdark that changes the color of the text depending on the current color scheme.
  • One property increment with type { value : number }, that allows to set up the increment value.
  • It emits a CustomEvent “IncrementedEvent” with the current value as a payload.

You can view the complete code here: web-counter

I chose vanilla JS for implementation - while this makes the code longer, it ensures pure compatibility with Web Standards.

In the React app, we want to:

  • change the color scheme between light and dark
  • switch the increment value between 1 and 2
  • track the total number of button clicks by subscribing to the “IncrementedEvent” CustomEvent

This is how it will look in the browser:

Light theme

Light theme

Dark theme

Dark theme

How we will work with this component in React 18 app?

Because this web component has a property of the type Object that we want to interact with, we need to create a Ref first, and create a useEffect to manually update values when the state changes.

const [increment, setIncrement] = useState({ value: 1 });
const counterRef = useRef(null);

useEffect(() => {
  if (counterRef.current) {
    counterRef.current.increment = increment;
  }
}, [counterRef, increment]);

<...>
  <web-counter ref={counterRef}></web-counter>
<...>

Also, we will use the Ref to subscribe to the custom IncrementedEvent to count the clicks.

useEffect(() => {
  const node = counterRef.current;
  const addClickCount = () => setClickCount(clickCount + 1);
  if (node) {
    node.addEventListener("IncrementedEvent", addClickCount);
  }
  return () => {
    node.removeEventListener("IncrementedEvent", addClickCount);
  };
}, [counterRef, clickCount]);

Finally, since isdark is a boolean attribute, we need to create a custom props object to conditionally add or remove this attribute.

const props = isDark ? { isDark: true } : {};

<...>
  <web-counter {...props}></web-counter>
<...>

The whole code of React 18 app can be found here: react-18

How we will work with this component in React 19 app?

React 19 introduces a new strategy for handling Custom Elements. By default, it checks if a component has a corresponding DOM property — if so, React assigns the value directly to that property. If not, React treats it as a HTML attribute, intelligently handling boolean attributes by adding or removing them as needed. React 19 also adds native support for Custom Events, allowing developers to add event listeners using the familiar “on + CustomEventName” convention, just like standard events such as onClick. So all of the above code needed for React 18 can be simplified in React 19 to:

<...>
  <web-counter
    increment={increment}
    isDark={isDark}
    onIncrementedEvent={() => setClickCount(clickCount + 1)}
  ></web-counter>
<...>

The whole code of React 19 app can be found here: react-19

So, let’s compare, how our code changed thanks to improvements introduced in React 19?

  • No need for using Ref and working with the DOM node directly.
  • No need for artificial properties object to work with boolean attributes.

All transformations are summarized on this picture:

image info link to a bigger image

Conclusion

As a developer who has had to work with and support Web components in React, I’m very excited about these changes! If you’ve already tried Web components in your React apps and DevEx didn’t feel right, I encourage you to try them again in your next React 19 app!