Web Components and React Components - A Comparison
When I first started working with React, Web Components weren't a serious contender in the design systems space. But I can't help but notice more and more systems built with them, like Duet and Carbon Web Components, to name a couple.
At Wayfair, I work on our design system's React component library, which is great since most teams are working with React! However, many teams we support also manage legacy applications that are not written in React, and unfortunately, we can't do much to support them. However, I've recently begun thinking about Web Components, considering that they could be used in virtually any web appliaction, and could be a better format for our components in terms of supporting most, if not all, teams. Not having much experience with it, I decided to dig into understanding how Web Components work and how to build them.
Like I usually do, I looked at the source code for Web Component driven libraries like the two mentioned above, and I was surprised that literally none of them were built on the native API directly. They all used compile-time libraries like StencilJS to smooth over the developer experience.
So what's so bad about the the Web Component API? I decided to find out. I started by just building a few really simple web components to understand how they work and their limitations. Let's go over how they compare to building a component with React!
If you just want to see some code, here's a repo I used as a playground for web components.
Web Components or Custom Elements?
I think these two terms are often confused, but they're actually different things. Web components are custom elements, but custom elements aren't necessarily web components.
According to MDN, custom elements are:
Here's what helped me wrap my head around what "custom" really means: you are literally creating a brand new element and API for the DOM. That means you are responsible for defining what the element's valid attributes are, and when they change, what happens, when callbacks are exposed, etc. You can leverage built-in elements, but you are still responsible for connecting the two together manually. I'll go into further detail below, but you can maybe start to see why this API can be tedious (compared to React).
Web Components, on the other hand, use the custom elements API, but also include two other web technologies: the Shadow DOM, which basically keeps the custom markup hidden from the rest of the document, like an
iframe. and HTML Templates, markup that is added to the DOM without rendering anything, but can be leveraged to bootstrap the markup for a custom element.
The real meat-and-potatoes of Web Components are custom elements, so I'll mostly dig into that here, and touch on the others.
React and Web Components: Compared
First of all, building web components is extremely similar to working with React class components—surprisingly so. In case you haven't looked at a React class component in a while, here's a refresher:
And here's the basic outline of a Web Component. Spot the 5 differences! 😂
Wait, how do we keep track of internal mutable values? Where do we render the markup?? Let's dig into the web component line-by-line, and discuss what's going on.
A Web Component (or custom element) starts by declaring a new class that extends
HTMLElement, a global object in the browser that you can think of as a primitive for all other HTML elements. You can actually extend real HTML elements to inherit their qualities (like
HTMLParagraphElement), but that's for another time!
We're already getting a taste of getters and setters! Similar to
propTypes, in this field we define what HTML attributes our component should react to in changes. For example, if we say our component has a
type attribute that could change after the component is rendered:
...we should define
'type' in this array. We'll discuss how to react to this change in
connectedCallback & disconnectedCallback
These two methods draw the clearest comparison to React classes' lifecycle methods. They will most often be used for handling event listeners, fetching data, clean-up, etc.
When one of the attributes that was declared in
observedAttributes changes, this callback will fire with the name of that attribute and its new and old values. Pretty similar to
componentDidUpdate! Usually this callback is used to trigger side-effects, like properties or element attributes of our component changes in reaction to attributes on the custom element changing.
For example, if our aforementioned example of
type changed, we could use it to update the
type attribute on a
<button> element the Web Component rendered:
Whew, we've gone all this way without discussing state or markup! Pretty much all of this happens in the class' constructor. Since this topic requires the most detail, let's dig into an example Web Component, where we can tie all of the API together.
Simple Button Component API
Let's say we want to create a custom button component. It should expose its click event to the application, as well as the
disabled, and its nested content should render within the button element. In React, this is pretty straightforward:
In this example, nothing crazy is going on, and we don't have to use any of the lifecycle methods outlined above. We more or less just pass down the props we want to expose on the
As we'll see, this is not as straightforward with Web Components.
Rendering the markup
Let's talk about how we render markup with Web Components, including the attributes. Here's the API we want to accommodate:
First, let's render our
button element, and make sure that when we render text within the component, that is rendered inside the
FooButton is itself an element, so we can simply use
this.innerHTML to set the markup. But what is
slot element is A LOT like the special
children prop in React. When we render a
slot element within our custom element, whatever is rendered within the custom element will be presented where the
slot is placed. The content of the slot element is what's rendered if nothing is rendered with the custom element.
You can use multiple
slot elements by assigning them
name attributes, and then the implementation can distinguish slots by using the
slot attribute. For example, let's say we have a Card component with content and a title:
We can distinguish what content goes where by using
Up to this point, it looks like web components and React components have a lot of similarities. We can use
slot elements in place of React's
This would find the
<button> element in the DOM, set its disabled property to true, and disable interaction with the element as we would expect. With custom elements, we must explicitly define
disabled as an accessor property, meaning it can be both read and written. With classes, we use "getters" and "setters". Let's expose these properties on our component:
Whew, that's a lot! And this is just two attributes! We would need to define these getters and setters for every attribute that can be manipulated on the element. And we also just reached a big hitch—just because the custom element's attributes can be read and written, this doesn't mean that the
<button> our component renders will inherit those attributes! We must explicitly update this element ourselves. We can do this by leveraging the
changedAttributesCallback methods. Here's how we might set that up, with the getters and setters omitted for readability:
In summary, in order to support attributes in a similar fashion to a React component with Web Components or custom elements, we must do the following:
- Define setters and getters for each attribute that makes the properties readable and writable.
- Monitor for their changes by defining them in
- Update the markup of our component via
Now our button Web Component supports custom attributes and can accept "children" to change its text content. Now it's time to expose its click callback!
Exposing event callbacks
Let's say we want our FooButton component to have a 'click' event like a regular
<button> element. For example:
In React, we could just expose the
onClick prop on the button element within our component. Not so for Web Components!
Similar to regular attributes, we must handle exposing the click event on the component, and also wiring up the event to the
<button> element it renders.
Woah, custom events! This was my biggest surprise when setting up a Web Component. In React, you can forward a function prop to an element's native event callback (e.g.,
onClick). With Web Components, you must define your own custom events in order to control how and when they're called. Since we want our custom event to be called when the
<button> is clicked, we create and clean-up the event listener in
Note that this does not add support for applying a function to the
onclick attribute of the custom element. That gets even weirder, and I'm not going to dive into that in this article.
Step it up with the shadow DOM
We pretty much have everything in our Web Component working! But we can take it one step further with encapsulation, meaning that the component's markup is insulated by direct manipulation from the outside.
I mentioned earlier that Web Components technically also encompass the Shadow DOM, and that's one feature that React components don't offer out of the box. By rendering our markup in the Shadow DOM, it's a bit like rendering in an iframe: it's not accessible from the rest of the document, meaning everything our component renders is truly encapsulated (and the API is strictly on the custom element). Our FooButton component doesn't use the Shadow DOM yet, but we can change that with a few lines:
Now when referencing the DOM of the web component, use
this.shadowRoot instead of just
The Final Component
Here it is in one code snippet, in all its glory!
I now understand a bit more how much React handles all of the render updates and event management. I always knew that React manages this stuff for me, but it's a fun good exercise to try and manage it yourself! Makes you appreciate it more. Web Components require a lot of boilerplate for reacting and updating the DOM when attributes change
Smoothing out the DX with lit-html, lit-element, Haunted, Stencil, and more
It's now super plain to me why all of these libraries exist to smooth out the development experience of Web Components. The most easiest to recommend is lit-html, which adds a React-like simplicity to rendering, updating when attributes change as they should. lit-element takes this a step further by providing a wrapped
HTMLElement class to extend, integrating lit-html, managaing the updating of properties and attributes, rendering into the shadow DOM, and optimizing styles.
If you really want to get React-like, Haunted is a full-feature implementation of React hooks into Web Components. No more classes! My mind is totally blown with this one.
And last but not least, likely the most popular toolkit, Stencil provides a whole toolchain for not only authoring the Web Components, but also for compiling outputs compatible with most popular JS frameworks, like React, Vue, and more.
Reference and Web Component Learning Material
- Web Components on MDN
- WebComponents.dev — super neat web IDE with templates for more than a dozen Web Component libraries!
- An Introduction to Web Components from CSS Tricks.
- Custom Elements That Work Anywhere by Rob Dodson
- Custom Element Best Practices from Google
- Line-by-line breakdown of a basic custom element from Google