We all know what React is today. For those who don't know it yet: it's a view rendering library that helps developers to build user interfaces rapidly.

In this article, I'm going to cover a topic of conversion of React components to Web Components. This might be useful in terms of delivering highly reusable pieces of code for people with almost no knowledge regarding frontend development, like content editors (we'll get a kind of HTML tag at the end) or in the case when you need to migrate some pieces of code to different JS frameworks.

This is the next part from a bigger idea of articles regarding framework agnostic UI approach. Previous articles:

Sample React component

Let's say we have a very simple component, that consists of a sample text and a button that can be interacted with:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import * as React from 'react' ; const SampleComponent = ( { name = 'Piotr' , onClickEventName } ) => { const ref = React. createRef ( ) ; return ( < div ref = { ref } > < p > Hi < strong > { name } </ strong >! I 'm a React component! </p> <button type="button" onClick={() => { ref.current.dispatchEvent( new CustomEvent(onClickEventName, { bubbles: true, cancelable: false, composed: true, detail: {}, }) ); }} > Click on me! </button> </div> ); }; export { SampleComponent }; import * as React from 'react'; const SampleComponent = ({ name = 'Piotr', onClickEventName }) => { const ref = React.createRef(); return ( <div ref={ref}> <p> Hi <strong>{name}</strong>! I'm a React component! </p> <button type="button" onClick={() => { ref.current.dispatchEvent( new CustomEvent(onClickEventName, { bubbles: true, cancelable: false, composed: true, detail: {}, }) ); }} > Click on me! </button> </div> ); }; export { SampleComponent };

The component takes 2 props:

name - contains a user name

- contains a user name onClickEventName - contains a custom event name required for a sort of communication between a component and the rest of code, outside of React scope. The communication will be done by creating custom events and dispatching them on a specific DOM node.

React component converted to Web Component

In my previous article, where I presented a way of converting an Angular component to a Web Component I used Angular internal tools for that. In the case of React components, there's no such tooling (or I'm not aware of it). We will have to convert it by creating Web Component (article is in Polish) code manually.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 import * as React from 'react' ; import { render , unmountComponentAtNode } from 'react-dom' ; import htmlToReact from 'html-to-react' ; import { SampleComponent } from './SampleComponent' ; class ReactElement extends HTMLElement { constructor ( ) { super ( ) ; this . observer = new MutationObserver ( ( ) => this . update ( ) ) ; this . observer . observe ( this , { attributes : true } ) ; } connectedCallback ( ) { this ._innerHTML = this . innerHTML ; this . mount ( ) ; } disconnectedCallback ( ) { this . unmount ( ) ; this . observer . disconnect ( ) ; } update ( ) { this . unmount ( ) ; this . mount ( ) ; } mount ( ) { const propTypes = SampleComponent. propTypes ? SampleComponent. propTypes : { } ; const events = { } ; const props = { ... this . getProps ( this . attributes , propTypes ) , ... this . getEvents ( events ) , children : this . parseHtmlToReact ( this . innerHTML ) , } ; render ( < SampleComponent { ... props } />, this ) ; } unmount ( ) { unmountComponentAtNode ( this ) ; } parseHtmlToReact ( html ) { return html & amp ;& amp ; new htmlToReact. Parser ( ) . parse ( html ) ; } getProps ( attributes , propTypes ) { return [ ... attributes ] . filter ( ( attr ) => attr. name !== 'style' ) . map ( ( attr ) => this . convert ( propTypes , attr. name , attr. value ) ) . reduce ( ( props , prop ) => ( { ... props , [ prop. name ] : prop. value } ) , { } ) ; } getEvents ( propTypes ) { return Object . keys ( propTypes ) . filter ( ( key ) => /on([A-Z].*)/ . exec ( key ) ) . reduce ( ( events , ev ) => ( { ... events , [ ev ] : ( args ) => this . dispatchEvent ( new CustomEvent ( ev , { ... args } ) ) , } ) , { } ) ; } convert ( propTypes , attrName , attrValue ) { const propName = Object . keys ( propTypes ) . find ( ( key ) => key. toLowerCase ( ) === attrName ) ; let value = attrValue ; if ( attrValue === 'true' || attrValue === 'false' ) value = attrValue === 'true' ; else if ( ! isNaN ( attrValue ) & amp ;& amp ; attrValue !== '' ) { value = + attrValue ; } else if ( /^{.*}/ . exec ( attrValue ) ) { value = JSON. parse ( attrValue ) ; } return { name : propName ? propName : attrName , value : value , } ; } } customElements. define ( 'sample-react-component' , ReactElement ) ; import * as React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import htmlToReact from 'html-to-react'; import { SampleComponent } from './SampleComponent'; class ReactElement extends HTMLElement { constructor() { super(); this.observer = new MutationObserver(() => this.update()); this.observer.observe(this, { attributes: true }); } connectedCallback() { this._innerHTML = this.innerHTML; this.mount(); } disconnectedCallback() { this.unmount(); this.observer.disconnect(); } update() { this.unmount(); this.mount(); } mount() { const propTypes = SampleComponent.propTypes ? SampleComponent.propTypes : {}; const events = {}; const props = { ...this.getProps(this.attributes, propTypes), ...this.getEvents(events), children: this.parseHtmlToReact(this.innerHTML), }; render(<SampleComponent {...props} />, this); } unmount() { unmountComponentAtNode(this); } parseHtmlToReact(html) { return html && new htmlToReact.Parser().parse(html); } getProps(attributes, propTypes) { return [...attributes] .filter((attr) => attr.name !== 'style') .map((attr) => this.convert(propTypes, attr.name, attr.value)) .reduce( (props, prop) => ({ ...props, [prop.name]: prop.value }), {} ); } getEvents(propTypes) { return Object.keys(propTypes) .filter((key) => /on([A-Z].*)/.exec(key)) .reduce( (events, ev) => ({ ...events, [ev]: (args) => this.dispatchEvent(new CustomEvent(ev, { ...args })), }), {} ); } convert(propTypes, attrName, attrValue) { const propName = Object.keys(propTypes).find( (key) => key.toLowerCase() === attrName ); let value = attrValue; if (attrValue === 'true' || attrValue === 'false') value = attrValue === 'true'; else if (!isNaN(attrValue) && attrValue !== '') { value = +attrValue; } else if (/^{.*}/.exec(attrValue)) { value = JSON.parse(attrValue); } return { name: propName ? propName : attrName, value: value, }; } } customElements.define('sample-react-component', ReactElement);

In the code sample above there's a lot going on. Besides the default Web Components methods like:

constructor ,

, connectedCallback ,

, disconnectedCallback

There are also custom methods called: update , mount and unmount . These 3 methods are crucial to make a React component wrapped by a Custom Element working.

First of all, when a new element is placed inside the HTML code the connectedCallback method runs mount method internally. The method is responsible for conversion of HTML attributes into proper React props.

The getProps method analyses the props definition of a component and based on that it converts Custom Element's attributes into React props.

Similarly does the getEvents method. The main difference is that we cannot pass the function reference in HTML attributes, because attributes' values are set as string type. In order to run a specific action when something happens inside the React component we need to use JavaScript events. Instead of passing an event callback reference, we're passing an event name that will be dispatched when some action takes a place.

Additionally, we're also converting inner HTML code of a Custom Element into React children, but keep in mind such conversion is costly. If the inner HTML code is changing extremely frequently like during the drag action, it's better to avoid such conversion and place that HTML code as a value of dangerouslySetInnerHTML prop inside the React component.

At the end we're defining a new Custom Element by declaring it like the following:

1 customElements. define ( 'sample-react-component' , ReactElement ) ; customElements.define('sample-react-component', ReactElement);

Our new HTML tag will be and it's assigned to ReactElement JavaScript class. It means, that whenever someone places tag inside HTML code it will create a new instance of ReactElement class and mount its functionality to the HTML node.

Usage

Let's use our freshly created new HTML tag somewhere inside the HTML structure of an app or any website:

1 <sample-react-component name="Sample user" onClickEventName="doSomething" /> <sample-react-component name="Sample user" onClickEventName="doSomething" />

And that's it! Using custom element HTML tags is as simple as using standard HTML tags. There's no difference.

Summary

After reading this article you should know how to convert React components into Web Components. Such knowledge will help you maintain your codebase in the long term as there might be a need to move some parts to a new frontend technology stack that might appear at some point in the future. Web Components API is a browser native technology, hence it's supposed to last longer than any existing JS library or framework.