Web Components vs. React

To get started there are 3 main terms that we need to know :

  • Web Components:

    Components are the building blocks of modern web applications. They are build using:

    1. Custom Elements

      custom elements allow you to extend HTML and define your own tags. They are considered low level.

    2. Shadow DOM

      shadow DOM is a web standard that offers component style and markup encapsulation. It is a critical piece of the Web Component

First off let's have a quick look at how web components work, and then we'll dive into how they can be used alongside react.

Web Components

Here's a very quick introduction to creating Web Components. Following is is a transcript of the code which is plain vanilla javascript, no React yet.

  • We bootstrap our app by listening to the load event. This avoids delaying the initial rendering.

    window.addEventListener('load', () => {
      myApp();
    });
    
  • Our app is an async function which uses await for server API calls:

    async function myApp() {
    
      const res = await fetch('https://mydomain.com/api');
      const json = await res.json();
    
      // get DOM reference for where we will plug our custom
      // html elements
      const main = document.querySelector('main');
    
      // create custom html elements and plug them into DOM
      json.articles.forEach(article => {
        const articleDOMElement = document.createElement('mycustom-html-element-tagname');
    
        articleDOMElement.render(article);
    
        main.appendChild(articleDOMElement);
      });
    }
    
  • Notice the use of document.createElement('mycustom-html-element-tagname')?

  • The above will only work properly, once we define the custom element in javascript. To do that, you have to create a class for your custom element extending HTMLElement. Name the class however you want. Then use the customElements.define('mycustomtagname', mycustomelementclassref) to let the browser know how to create HTML Elements whose tag name is 'mycustomtagname':

    class MyCustomHtmlElement extends HTMLElement {
      // ... we define content later
    }
    customElements.define('mycustom-html-element-tagname', MyCustomHtmlElement);
    
  • Inside the MyCustomHtmlElement class, we need to define at least how our custom HTMLElement will display itself. Usually you'll also want to pass data to the custom element. Here we decided to name our method render(article), it is responsible for populating the custom element (but the name is completely up to you):

    class MyCustomHtmlElement extends HTMLElement {
    
      render(article) {
        this.innerHTML = MyCustomHtmlElement.template(article);
      }
    
      static template(article) {
        const {url, title, imageUrl, description} = article;
        return `
          <a href="${url}">
            <h2>${title}</h2>
            <img src="${imageUrl || ''}" alt="article illustration image">
            <p>${description || ''}</p>
          </a>
        `;
      }
    
    }
    customElements.define('mycustom-html-element-tagname', MyCustomHtmlElement);
    
  • Shadow DOM is a feature of Web Components, allowing you to encapsulate and hide the inner details of your custom element, to make use of shadow DOM simply move everything from the HTMLElement component itself to its shadow:

    class MyCustomHtmlElement extends HTMLElement {
      // We need to attach a shadow to a property
      constructor() {
        super();
        this.root = this.attachShadow({mode: 'open'});
      }
    
      // then render on that property instead of 'this'
      render(article) {
        this.root.innerHTML = MyCustomHtmlElement.template(article);
      }
    
      // ... same as before
    
    }
    customElements.define('mycustom-html-element-tagname', MyCustomHtmlElement);
    
  • The current implementation will work, but since it will shield all the details of our custom element from any external part, the CSS that we had won't be applied. To solve this we need to define our CSS within our component. This will also shield the CSS from the outside :

    class MyCustomHtmlElement extends HTMLElement {
    
      // ... same as before
    
      static template(article) {
    
        const {url, title, imageUrl, description} = article;
    
        // Everything that is returned will be inside our custom element tagname
        return `
          /* we add styles within the <mycustom-html-element-tagname>
           * so no need to add mycustom-html-element-tagname as a selector
           */
          <style>
             a > h2 {
                font-size: 2rem; /* can I use rem here? */
             }
          </style>
          <a href="${url}">
            <h2>${title}</h2>
            <img src="${imageUrl || ''}" alt="article illustration image">
            <p>${description || ''}</p>
          </a>
        `;
      }
    }
    customElements.define('mycustom-html-element-tagname', MyCustomHtmlElement);
    
  • The end result in the browser inspector will be:

    <!-- ... -->
    <mycustom-html-element-tagname></mycustom-html-element-tagname>
    <mycustom-html-element-tagname>
      #shadow-root (open)
        <style>
           a > h2 {
              font-size: 2rem; /* can I use rem here? */
           }
        </style>
        <a href="${url}">
          <h2>${title}</h2>
          <img src="${imageUrl || ''}" alt="article illustration image">
          <p>${description || ''}</p>
        </a>
    </mycustom-html-element-tagname>
    <mycustom-html-element-tagname></mycustom-html-element-tagname>
    <mycustom-html-element-tagname></mycustom-html-element-tagname>
    <!-- ... -->
    

This was a quick overview of how to implement a custom element which uses shadow DOM.

Web components tooling

While searching for components on webcomponents.org, I've come across Storybook, which is a so called Component explorer it :

helps you build UI components isolated from the business logic and context of your app. Storybook

To do so, it provides a web interface to explore the UI components of your application.

Consider a component explorer a dictionary for your modular UI that lives alongside your production app[s]. HiChroma

Storybook fits into what is called Component Driven Development.

Component-Driven Development (CDD)

Component-Driven Development (CDD) is a development methodology [...] that builds UIs from the "bottom up" by starting at the level of components and ending at the level of pages or screens. [...] working one component at a time in a fixed series of states. HiChroma

CDD solves:

  1. Isolation: Instead of trying to get a single component into a certain state by mingling with the whole application, CDD allows you to work on your component in isolation.
  2. Test every state: because CDD requires you to work on every possible state of your component, you can be confident that no unexpected state will come up. See how it's done here
  3. Developer designer relationship: designers understand graphical output, and Component explorers bridge the gap between code and design.
  4. Reuse your components: build once use everywhere.
  5. Parallelize development: working one component at a time allows you to share tasks between different team members in a way that is just not possible at the level of "screens".
  6. Visual Testing: component explorers allow you to test components visually. paraphrased from HiChroma

How to CDD

  1. Install React Storybook if are using react.

    npm i -g getstorybook
    cd app
    getstorybook
    
  2. Add a story (a test state) to the component you are working on by creating a file like src/components/stories/mycomponent.js:

    import React from 'react';
    import { storiesOf } from '@kadira/storybook';
    import Component from '../mycomponent.js';
    
    storiesOf('MyComponent')
      .add('low data', () => <MyComponent with="some" ofYourProps={you('would like to test')} />)
    

    See more detailed example here.

  3. Then npm run storybook

  4. Navigate to the storybook app to explore your component

  5. Then you can repeat the process of adding stories (2.)

Once you start composing your components, knowing that you have tested each building block separately and thoroughly will give you the confidence to start building up.

On top of a component explorer, there are services that allow you to monitor changes to the ui. It will automatically run your stories for you on the cloud and take snapshots of the UI while being tested. If there happens to be a pixel diff between the "last known good state", it will notify the developer who can directly investigate. See Diff detector from Chromatic

React and Web Components

Most of the following content is taken from this video. Please make sure to check it out. It has not been reviewed nor endorsed by the original author, may contain inexactitudes.

Custom Elements

Custom elements are just a custom tag. Instead of using a div for example you may want to use my-custom-el. The only requirement is that your custom element should have a - in its name.

In terms of support is pretty good.

Shadow DOM

Lets you encapsulate all the details. Also a nice point is that events, like a click event will not bubble up the hierarchy; it will stop at that Shadow element.

Its support is ok. In case some features are not yet supported, there are polyfills, or this one.

The debate

If your team is using React or Angular, it could make sense to build components exclusively through the library (e.g. using React Components or Angular components). It might be easier to stick to a single paradigm or model.

On the other hand, if you have a mixture shop, meaning that some are using Angular and others are using React others Vue and some vanilla, then the Web Component paradigm is the only one that you have that is across all those different approaches. And so it gives you a way to share components across teams.

Now you end up wondering, where does React fit in with Web Components? Very simple, treat the component as another html element. Instead of rendering a div in react, you simply render a web component instead, like my-element.

import React, {Component} from React

class App extends Component {
  render() {
    return (
      <div onWobble={e => console.log('event triggered', e)}>
        <span>Hello, world!</span>
        <my-element></my-element>
      </div>
    );
  }
}
export default App;

Of course for this to work you should define and import the web component class, like we did for mycustom-html-element-tagname above. Let's quickly create the web component to understand how the event onWobble works.

class MyElement extends HTMLElement {
  constructor() {
    super();

    this.root = this.attachShadow({mode: 'open'});
    this.message = document.createElement('div');
    this.message.textContent = 'Custom element!';
    this.addEventListener();
  }

  connectedCallback() {
    this.root.appendChild(this.message);
  }

  addEventListener() {
    this.root.addEventListener('click', () => {
      const event = new CustomEvent('wobble', {
        default: {
          m: '%c WOBBLE',
          s: 'color: red; font-size: 32px;',
        },
        bubbles: true,
      });
      this.dispatchEvent(event);
    });
  }
}

customElements.define('my-element', MyElement);

Our root property will experience events, but they stay shielded within our component. That is why we add event listeners to the root from within our component. And then dispatchEvent with our customized event. We also need to add: bubbles: true to allow parent elements to sniff it. So from our react component, since we added the <div onWobble={e => console.log('event triggered', e)}> we can intercept that event and do something with it.

Learn more about Web Components integration with libraries

This website has compiled sets of tests for each library to evaluate the support of certain features. Make sure to check it out if need to use web components along your library (like React).