Using Web Components with React in 2019
Web Components. As web developers, we’ve been hearing about them for a long time. In theory, they sound amazing: a browser-native way of defining your own custom components. You can read more about Web Components in our articles Web Components - An Introduction and Practical Usage and Web Components Introduction: Creating Custom HTML Elements in 2018.
JavaScript web development frameworks like React, Angular, and Vue have become popular because they take a lot of the grunt work out of building complex web applications. Most frameworks also support the concept of components: pre-configured code objects that extend the framework and give you code-once, drop-in functionality. But each framework defines its own way to create components. You’d have to build component versions of a custom feature for each framework you wanted to support. Or if you’re trying to find a pre-made component to use in your React app, but there was only an Angular component, or a Vue component, you’d have to roll your own React version of the component.
Web Components offer a universal solution to the problem of framework-specific components. With Web Components, creators of reusable component libraries can develop a single version of the component that can then be used easily no matter what front-end framework you’re using.
Do Web Components Make React Obsolete?
In a word: no.
As great as Web Components are, defining custom components that you can embed in a web page is all they do. React's main attraction is that it allows you to declaratively define a user interface that automatically stays up to date with state changes.
Web Components on their own don't offer anything equivalent to what React offers. And that's fine! It's not a failing; offering a declarative, reactive UI construction kit isn't part of the Web Components spec, nor should it be.
Web Components and tools such as React, Angular, and Vue complement each other nicely. If you're thinking of building web UI components that you'd like to share with others, consider creating them as Web Components. You can ship pure Web Components confident in the knowledge that developers will be able to use them regardless of what front-end framework they are using, or even if they're using no front-end framework at all.
You'll be able to reach a far wider audience with Web Components than targeting a single framework, and if your components become popular, there's nothing stopping you from creating framework-specific wrappers for them.
To demonstrate the ease of development and flexibility provided by Web Components, we’re going to walk through the creation of two different components. Then we’re going to use these components in a real React app. You’ll gain a deeper understanding of the Web Component specification to create your own, and also learn how to integrate existing Web Components into your React apps in the future. You’ll also see some of the tradeoffs between creating Web Components that can be used across JavaScript frameworks and developing React-specific components.
Prerequisites of using Web Components with React
The rest of this article is going to assume you're familiar with modern React – up to and including the use of hooks.
I’m also going to assume you’ve read about Web Components (see the Web Components spec to learn more), have a high-level understanding of them, and may have even used them.
Finally, you should be familiar with web browser APIs in general, and the DOM specifically. Although we won't use DOM in great depth, a general understanding of DOM is will ensure you understand what's going on when we start discussing Shadow DOMs.
A Whirlwind Tour of Web Components
Let’s get down to the nuts and bolts of how Web Components work. Since our aim in this article is to understand how to use Web Components in a modern React application, we’ll just go over the high-level Web Components concepts necessary for this solution.
But if you're looking for a more in-depth introduction, the MDN Web Components Guide is a fantastic resource for detailed Web Components information.
The Web Components spec has four parts:
- Custom Elements – A set of JavaScript APIs that allow you to create your own HTML elements and define the way they behave.
- Shadow DOM – Essentially a private DOM that is scoped to your component, and your component only. This is handy for making reusable components because you can add CSS styling to your components without having to worry that your CSS class names will interfere with your component users' class names. Style changes made to the main DOM will be applied to the elements in your component's Shadow DOM, however.
- HTML Templates – New HTML tags that allow you create templates for your components. While not essential, templates are helpful when building larger components without having to create your whole Shadow DOM manually.
- HTML Imports – A browser API that allows you to import your HTML templates when a web page loads. The HTML Imports spec is still at the working draft stage, and browser support is inconsistent. Don't count on it being available in 2019. If you're reading this in 2020 or later, it's worth your time to check on its current status.
Of the four parts of the spec, Custom Elements and Shadow DOM are the most important. You can't really create a functioning Web Component without using those two.
HTML Templates streamline the creation of Shadow DOM elements for large, complicated, or frequently used components, but they do require defining both the template and the code for your component. If your components aren’t too large or complicated, it may be easier to just use ES2015 template strings to create your component’s UI. That’s exactly what we’re going to do in the next section when we create our components.
As mentioned, the HTML Imports spec is in flux and doesn't (yet) have much browser support. Chrome has marked its HTML Imports support as obsolete as of version 73, and Firefox doesn't yet ship support at all (current support for HTML imports).
Creating Simple Web Components
When using Web Components in the wild, you'll come across two common scenarios: components that receive data via attributes, and components that expose an imperative API that you can use to manipulate the component's internal state.
Components that are controlled via attributes will feel very familiar to React developers. Passing data in via attributes feels very much the same as creating React components via props. In most respects, it works very similarly – with a couple of caveats, which we'll discuss when we use our shiny new Web Components in React.
Components that only expose an imperative API are going to feel a bit foreign to React developers. Instead of being able to use them the way you'd use other React components, you're going to have to treat them like foreign interlopers and use React refs
to interact with the custom element's imperative API – which can include things like calling methods to alter the component's internal state or attaching event handlers so your React app can be informed when the custom element fires events.
Since we'll need to know how to handle both scenarios, we're going to create two custom Web Components: one that takes data via attributes, and one that only exposes an imperative API. We'll then walk through the steps needed to use both in our very own React app.
For a sneak preview of what we're going to end up with, you can see the final result here: https://react-webcomponents-2019.stackblitz.io/.
You can find all of the code we're going to be using on StackBlitz at https://stackblitz.com/edit/react-webcomponents-2019. If you haven't used StackBlitz, it's a great web IDE powered by Visual Studio Code that also hosts and runs your front-end code for you – so you can make your edits in the browser and see your app update in real-time.
Our components are going to be very simple: they'll be counters that display the current number their internal currentCount
variable is set to. This might sound too simple, but as you'll see, they're just complex enough to give us a good feel for how both types of components work.
In the StackBlitz workspace linked above, you'll notice two files in the web-components
folder: ImperativeCounter.js
and DeclarativeCounter.js
. These are our two custom Web Components. We're going to take a look at the code in each of them to understand what's going on.
We'll start with ImperativeCounter
:
class ImperativeCounter extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open'});
this.currentCount = 0;
this.update();
}
update() {
this.shadow.innerHTML = `<div><b>Count:</b> ${this.currentCount}</div>`;
}
increment(){
this.currentCount++;
this.update();
}
decrement() {
this.currentCount--;
this.update();
}
}
window.customElements.define('i-counter', ImperativeCounter);
What we see above is an HTML custom element. As described in the previous section, all custom elements must extend HTMLElement
, so that's exactly what our class does.
In the constructor, we attach the Shadow DOM, define an instance variable named currentCount
, and call the class' update
method.
If we look at what update
does, we see that its task is simple: it updates the innerHTML
of our element's Shadow DOM based on the value of currentCount
.
Next, we have two methods: increment
and decrement
. They are both very simple; they either increment or decrement the value of currentCount
, and then call update
to refresh the element's Shadow DOM with the updated count.
And that's it for ImperativeCounter
. In the final line of code, we call the browser's Custom Elements API to register our new element with the tag name i-counter
. When calling define
, your component's tag name must contain a hyphen. The browser will throw an exception if there's no hyphen. The official Custom Elements spec contains a detailed explanation of what constitutes a valid tag name.
Now, let's take a look at DeclarativeCounter
. You can find it in DeclarativeCounter.js
in the StackBlitz workspace. This element works a bit differently than our imperative counter. Instead of calling methods to update the counter's state, we'll just need to set the value of the element's count
attribute – in much the same way we set a prop on a React element.
As a result of this, the mechanics of our custom element class are a bit different:
class DeclarativeCounter extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open'});
}
static get observedAttributes() {
return ['count']
}
attributeChangedCallback(attrName, oldVal, newVal) {
this.currentCount = newVal;
this.update();
}
connectedCallback() {
this.currentCount = this.getAttribute('count') || 0;
this.update();
}
update() {
this.shadow.innerHTML = `<div><b>Count:</b> ${this.currentCount}</div>`;
}
}
window.customElements.define('d-counter', DeclarativeCounter);
We begin by attaching the element's Shadow DOM in the class constructor. Note that we didn't do any additional setup in the constructor. This is because reading element attributes in the constructor isn't reliable – it'll probably work if you're using the custom element directly in HTML, but as of React 16.8, it won't work while using your custom element in React. Because of this, we'll defer the reading of the count attribute until later in the component's lifecycle.
Next, we see a static property getter called observedAttributes
. We add this getter to tell the browser the attributes for which our element wants to be notified when the property is changed. If we don't do this, the browser will inform the element when any of its attributes change, which usually isn't a problem, but might make the browser do more work than it needs to.
This getter must return an array of strings that represent the names of the attributes our element cares about. Our declarative counter only cares about the count
attribute, so we return an array containing a single string. As we'll see in a moment, telling the browser we only care about changes to count
will let us use a nice little shortcut.
Next up, we see an instance method named attributeChangedCallback
. This is part of the Custom Elements API and is called whenever one of the attributes our element has said it cares about is updated. It takes three parameters: the first contains the name of the attribute that was updated, the second contains the old attribute value, and the third contains the new attribute value.
Normally, if a custom element is observing changes on several attributes, you'll need to write code to check which attribute changed before you decide what to do. Here's where the shortcut comes in: since DeclarativeCounter
told the browser that it only cares about changes to count
, its attributeChangedCallback
will only be called when count is updated.
So, we skip checking the name of the attribute that was changed, and simply update the class' currentCount
instance variable, then call update
to refresh the element's Shadow DOM.
Next, we see a method named connectedCallback
. This callback is another part of the Custom Elements API. It is called after the browser creates a custom element and adds it to the DOM. Here, we do a bit of setup for our element – we set the value of the currentCount
instance variable based on the value of the element's count
attribute, with a fallback of setting it to 0 if count
is missing. Once we've done this, we call update to set the initial state of the element's Shadow DOM.
With that, all of the hard work is out of the way. The update
method in DeclarativeCounter
is identical to the one in ImperativeCounter
, and that's it for the DeclarativeCounter
class.
We make one final call to register the custom element under the tag name d-counter
. Now that we've created and registered our components, let's see how we'd use them in a React app.
Adding Our Components to a React App
Now that we've created some web components and understand how they work, it's time to put them to use in a real, live React app!
We’re going to focus on using Web Components in a functional React component using hooks. Hooks may be new to React, and while there are some dissenting voices who don’t like them, the majority of the React community seems to be all-in on hooks.
We've been here before. Way back in 2013, the only way to create React components was by calling React.createClass
. Eventually, class-based components appeared. Suddenly, everyone was writing their components using ES2015 classes.
It feels like hooks are following a similar trend. Using them and understanding them will help you stay ahead of the game in the fast-changing JavaScript and React world.
If you've never used hooks in React, fear not – I'll also include a component that demonstrates how to use our custom components in a class-based React component so you can compare the two approaches. You'll see that they aren't all that different.
Let's take a look at the code. You can find it all on the same StackBlitz workspace where we examined our Web Components: https://stackblitz.com/edit/react-webcomponents-2019. Open up the components folder, and then open the CounterHook.js file. This is what you'll see:
import React, { Component, useState, useRef } from 'react';
import NavBar from '../containers/NavBar';
import Card from '../containers/Card';
import CounterBox from '../containers/CounterBox';
export default () => {
const counterElement = useRef(null);
const [count, setCount] = useState(0);
const incrementCounters = () => {
// increment the declarative counter
setCount(count + 1);
// increment the imperative counter
counterElement.current.increment();
}
const decrementCounters = () => {
// decrement the declarative counter
setCount(count - 1);
// decrement the imperative counter
counterElement.current.decrement();
}
return (
<>
<NavBar title="Web Components + React = ❤️" />
<Card title="Web Component Counters">
<CounterBox>
<h5>Imperative Counter</h5>
<i-counter ref={counterElement}></i-counter>
</CounterBox>
<CounterBox>
<h5>Declarative Counter</h5>
<d-counter count={count}></d-counter>
</CounterBox>
<button
onClick={incrementCounter}
className="btn btn-primary">Increment</button>
<button
onClick={decrementCounter}
className="btn btn-primary">Decrement</button>
</Card>
</>
)
}
There's nothing too exciting at the beginning. We import React, and then import the useState
and useRef
hooks. If you're still getting used to hooks and need a refresher, React's documentation has a great introduction to hooks. We then import a few Bootstrap-based container components that will make our final counter app a little bit prettier:
import React, { Component, useState, useRef } from 'react';
import NavBar from '../containers/NavBar';
import Card from '../containers/Card';
import CounterBox from '../containers/CounterBox';
Inside our component, we set up our two hooks:
const counterElement = useRef(null);
const [count, setCount] = useState(0);
These are simple: we're setting up a ref
that React will populate when it mounts the component and creating a bit of state named count
with a default value of 0 and a setter named setCount
.
Next, we have two functions: incrementCounters
and decrementCounters
. Let's take a closer look at these, because they illustrate the difference between working with imperative and declarative web components. First, incrementCounters
:
const incrementCounters = () => {
// increment the declarative counter
setCount(count + 1);
// increment the imperative counter
counterElement.current.increment();
}
For the declarative counter, we're just using setCount
to update the value of the React component's state. This is identical to what we'd do if our declarative Web Component were a React component that received its count via props. And that makes sense – in both cases, the component itself is stateless and is having its count value pushed in via props (for React components) or attributes (for Web Components).
For the imperative counter, things are a bit different. Its state is internal, so we can't push it in via an attribute. Instead, we must call the component's increment
method. The only bit that might look strange is the call to current
. It is part of React's refs API. A call to the useRef
hook (or to React.createRef
in a class-based component) returns an object with a current
property that stores the physical DOM element that is assigned in the component's JSX template.
Next, we see that decrementCounters
is exactly the same, except we're decreasing the counter values instead of increasing them:
const decrementCounters = () => {
// decrement the declarative counter
setCount(count - 1);
// decrement the imperative counter
counterElement.current.decrement();
}
Finally, we have the JSX markup that renders our counters:
return (
<>
<NavBar title="Web Components + React = ❤️" />
<Card title="Web Component Counters">
<CounterBox>
<h5>Imperative Counter</h5>
<i-counter ref={counterElement}></i-counter>
</CounterBox>
<CounterBox>
<h5>Declarative Counter</h5>
<d-counter count={count}></d-counter>
</CounterBox>
<button
onClick={incrementCounters}
className="btn btn-primary">Increment</button>
<button
onClick={decrementCounters}
className="btn btn-primary">Decrement</button>
</Card>
</>
)
Most of the markup is devoted to creating the user interface, so let's zero in on the critical parts:
<i-counter ref={counterElement}></i-counter>
Here, we're creating an instance of the imperative counter element we defined in ImperativeCounter.js
. Notice that we're using the ref attribute to assign the element ref to the variable we created earlier.
<d-counter count={count}></d-counter>
And here, we're creating an instance of our declarative counter. All we're doing is passing in our React component's count
state value to the declarative counter's count
attribute. As mentioned earlier, this is exactly the same way you'd pass state to a React component via props.
Finally, we add two buttons to increment and decrement our counters:
<button
onClick={incrementCounters}
className="btn btn-primary">Increment</button>
<button
onClick={decrementCounters}
className="btn btn-primary">Decrement</button>
These buttons just call the incrementCounters
and decrementCounters
functions. And … we're done! The rest of the app is just standard React app setup boilerplate that you've seen before. If you take a peek in index.js, you’ll see that we import our two Web Components before any React components try to use them:
// import custom elements
import './web-components/ImperativeCounter';
import './web-components/DeclarativeCounter';
As long as any web components you want to use are loaded and registered before your React app tried to render them, you're good to go. The final result is an application that looks like this:
You can see the final counter app running live here: https://react-webcomponents-2019.stackblitz.io/
Try it out and notice that React is able to keep both of our Web Component counters in perfect sync despite the fact that they work very differently under the hood.
As promised, I've also added a class-based equivalent to our hook-based React component. You can find it in components/CounterClass.js
. It looks and works identically to the hook-based component. Its code is just a little bit more verbose.
Web Components with React - Conclusion
And that's it! We’ve learned more about how Web Components work, created our own web components, and learned how to use them in a React app.
You’re now ready to mix Web Components into your apps and make them even more awesome than they already are! Using Web Components with React helps you deliver the best apps possible because you aren’t limited to using React components. For example, if you find a mapping, data visualization, or text editing component that’s only available as a Web Component, you can import it and use it your React application immediately.
For further in-depth reading on Web Components, I recommend the MDN Web Components page
. It serves as a great introduction to Web Components, and many links that will let you explore every aspect of Web Components in great detail.