Wrapping React Components Inside Custom Elements

Gil Fink
5 min readJun 16, 2019

--

Web Components

This week I had the pleasure of speaking in the ReactNext 2019 conference. My talk was called “I’m with Web Components and Web Components are with Me” and it was all about consuming Web Components in React apps and wrapping React components with custom elements. In this post I’ll explain the second part and why you might want to do it.

React and Web Components

React Documentation mentions that React and Web Components are complementary to each other. React is the view engine that is responsible to keep the DOM in sync with the app’s data, while Web Components provide a strong encapsulation for the creation of reusable HTML components. In real world, however, the two technologies are rarely combined together. For instance, I consult to several companies and none of them is using both React and Web Components. What could be the main reasons for that?

  • Developers are still suspicious about the Web Components API and prefer to use a proven framework/library instead.
  • Web Components API is still not implemented in some of the browsers, which means that in order to use them we need to load a polyfill code.
  • As developers, we are used to frameworks/libraries goodies such as data binding, reactivity, lazy loading and more. In Web Components we need to craft everything and the boilerplate is sometimes cumbersome.

So why invest in Web Components at all? You can find the answer in an article I posted back in 2017: “Why I’m Betting on Web Components (and You Should Think About Using Them Too)”. To summarize what I wrote — Web Components can help you decouple the implementation of your component from the framework/library and help you create a boundary between the components and their consuming app. They are also suitable for design system building which can be consumed by any framework/library.

Wrapping React Component inside a Custom Element

Now that we have a better understanding why would we want to use Web Components, let’s talk about how to take advantage of the Web Components API to wrap a React component.

We will start with a simple collapsible panel written in React:

The component includes a collapsible section and a header element that when clicked on toggles between collapsed and shown states. If we want to wrap this component inside a custom HTML element we need to take care of a few things:

  • Pass the title and children props
  • Re-render when the title prop is changing

We will start by creating the custom element class and by defining it in the CustomElementRegistry:

export default class CollapsiblePanel extends HTMLElement {}window.customElements.define('collapsible-panel', CollapsiblePanel);

Our class will include 2 members the title and mount point, which will be responsible to hold the mounting point in the DOM:

mountPoint: HTMLSpanElement;
title: string;

Now let’s talk about the main implementation point — mounting the React component. We will use the custom element’s connectedCallback life cycle event to do that:

connectedCallback() {
this.mountPoint = document.createElement('span');
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(this.mountPoint);

const title = this.getAttribute('title');
ReactDOM.render(this.createCollapsed(title), this.mountPoint);
retargetEvents(shadowRoot);
}

In the connectedCallback, we will create a span which is going to be our mounting point. Then, we will use the attachShadow function to create a shadow root which will be our boundary between the app and the React component. We will append the mounting point to the shadow root. After we set all the ground, we will use ReactDOM to render the React component (using the createCollapsed function that you will see in a minute). Last but not least, we will use a function called retargetEvents which is part of the react-shadow-dom-retarget-events module. We will get to why I’m using retargetEvents later in this post so keep on reading :).

Let’s look at the createCollapsed function:

createCollapsed(title) {
return React.createElement(CollapsibleReact, { title }, React.createElement('slot'));
}

The function is getting the title which will be used by the React component. Then, the function uses React’s createElement function to create the CollapsibleReact component instance. The createElement also receives the props object as a second argument and the children prop as third argument. In order to pass the children as expected I use the HTML slot element to make a bridge between the wrapping component children and the wrapped component children.

Now that we finished the mounting of the wrapper component, the next step is to re-render the component if the title changes. For that, we will use an observed attribute and the attributeChangedCallback custom element life cycle event. Here is how they are used in the component:

static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'title') {
ReactDOM.render(this.createCollapsed(newValue), this.mountPoint);
}
}

When the title changes we use ReactDOM render function again. Since we saved the mounting point, ReactDOM will do all the re-rendering heavy lifting and will calculate the diffs for us.

The custom element’s entire implementation:

Re-targeting React Events

Why did I use the retargetEvents function? React event system is relying on synthetic events, which are wrappers on top of the browser native events. All the events in React are pooled and will be registered on the document itself. That behavior can be very problematic when you use shadow DOM. In shadow DOM, the shadowed DOM fragment exists in its own DOM fragment. That means that React events won’t work inside the shadowed part. The retargetEvents function helps to register the events inside the shadow DOM and to make them work as expected.

Testing The Wrapper

Now we can test the wrapper component. I used an Angular application to consume the component and this is the code I used in the application main HTML:

The result of running the app:

Angular app hosting a React wrapped component

If you are interested in the full session from ReactNext 2019, you can watch it here:

Summary

You have just learned how to wrap a React Component as a Web Component, and saw how easy it is to consume the result from an Angular app. The approach I presented here is just one way to achieve this. Can you think of a different approach? How would you automate this process?
Please feel free to share your thoughts and comment!

Thanks to Adam Klein, Uri Shaked and Ran Wahle for reviewing the post before it was published!

--

--

Gil Fink

Hardcore web developer, @sparXys CEO, Google Web Technologies GDE, Pro SPA Development co-author, husband, dad and a geek.