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
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.
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 Ref
s 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
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:
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!