Using Web Components With React.js

React and web components are complementary technologies that can help us build a more modular, future-proof frontend. Discover when you might choose web components over React and how to use them together.

React.js is a JavaScript framework used to build complex frontend applications. It provides convenient methods for managing state (i.e. memory) and lays out a pattern for maintaining reusable components. This is a great tool for complex features with many moving pieces.

Web components are reusable elements built using web standards and ubiquitous browser technology. They are future-proof and accessible to any developer who possess fundamental skills in HTML, CSS, and JavaScript.

 


 

Why Choose Web Components?

 

Scalable

After a web component has been created by a specialist, it can be used and configured by anyone with basic HTML experience. These are a great solution when you need the component to be freely added by content authors, backend developers, or through automation.

Tech-agnostic

Web components are built with HTML and JavaScript and can be used across codebases regardless of their architecture or framework. They’re great for helping platforms or development agencies organize and reuse their favorite patterns across varying applications.

Encapsulation

Because each component is a JavaScript class, they have their own state and can react to changes in that state. The Shadow DOM can be applied to ensure the component’s state is only affected by its own actions and can’t be touched by other javaScript, thus reducing the risk regression that comes with managing an ever-expanding codebase.

 


 

Advantages of Combining Web Components and React.js

 

Bundling Capabilities

As a project’s web components grow in number and complexity, it will be necessary to plug in third party libraries or create reusable utilities. You were likely bundling before this point, but now it’s essential.

If you’re already using React in your development, take advantage of the bundler you already have by importing web components into the React’s app.js.

Better Developer Experience & Code Consistency

If written in vanilla JavaScript, a web component’s HTML is best rendered using a string literal. With this method, you loose syntax highlighting on your component’s HTML. Without these visual cues, it can be difficult to find typos or unclosed tags.

Vanilla JS example

class CustomElement extends HTMLElement {
  constructor(){
    super();
    this.message = this.getAttribute('message') || '';
  }
 
  connectedCallback(){
    this.innerHTML = `
      <p class="alert">${this.message}</p>
    `;
  }
}
​
customElements.define("custom-element", CustomElement);

If you are already using React, you can use the JSX compiler that comes with the framework! To do so, use ReactDOM.render() at the end of connectedCallback(). You now gain syntax highlighting on the inserted HTML. This also keeps code styles consistent across the frontend.

React has an article on how to accomplish this along with a second example of implemention.

JSX Example

import React from 'react';
import ReactDOM from 'react-dom';
​
class CustomElement extends HTMLElement {
  constructor(){
    super();
    this.message = this.getAttribute('message') || '';
  }
 
  connectedCallback() {
    ReactDOM.render(
      <p class="alert">{this.message}</p>,
      this
    );
  }
}

customElements.define("custom-element", CustomElement);

 


 

Example: Form Input With Validation

 

Below is an example for further context. It is taken from a real-world example — though much code has been removed to avoid information overload. Form inputs are often challenging to manage. Using the web component method allows you to separate concerns and build a consistent experience across forms.

The validation layer (code not included) is a class that reads the input’s attributes and runs inline validation according to those settings. This complexity was added to show the power and modularity of this technique. The Shadow DOM is not attached in this instance because we’d like to adopt global styles and communicate with other scripts.

contact.html

<form>
  <custom-input label="Name"
                name="name"
                required
                type="text"
                validation-required-error="Please enter your name">
  </custom-input>
  
  <custom-input label="Email"
                name="email"
                required
                type="email"
                validation-pattern-error="Please enter a valid email"
                validation-required-error="Please enter your email">
  </custom-input>
  
  <custom-textarea label="Message"
                   name="message"
                   required
                   validation-required-error="Please enter your message">
  </custom-textarea>
  
  <button type="submit">
    Send Message
  </button>
</form>

<script src="bundle.js"></script>

react/src/components/form/input.js

import React from 'react';
import ReactDOM from 'react-dom';
// This class handles validation (code not included)
import CustomValidation from '../path/to/validationModule';
const expectedAttributes = ['label', 'name', 'type'] class CustomInput extends HTMLElement {
  constructor() {
    super();
    // don't use shadow dom. Component should take global styles access global js
    // this.attachShadow({ mode: "open" });

    // tell this component to valdiate. Trigger set in connectedCallback
    this.validate = () => {};
    this.label = {
      id: `${this.getAttribute('name')}Label`,
      text: this.getAttribute('label')
    };
    this.input = {
      attributes: {},
      id: `${this.getAttribute('name')}Input`,
      name: this.getAttribute('name'),
      type: this.getAttribute('type')
    };
    this.error = {
      id: `${this.getAttribute('name')}Error`,
      text: ''
    };
    
    // if unexpected attribute passed to component, assign to this.input
    this.getAttributeNames().forEach(attr => {
      if (expectedAttributes.indexOf(attr) === -1) {
        this.input.attributes[attr] = this.getAttribute(attr);
      }
    });
  }
  connectedCallback() {
    ReactDOM.render(
      <div className={`form-group--${this.input.type}`}>
        <label className={`form-group__label`} id={this.label.id}>
          {this.label.text}
        </label>
        <input aria-describedby={this.error.id}
               aria-labeledby={this.label.id}
               className={`form-group__input`}
               id={this.input.id}
               name={this.input.name}
               type={this.input.type}
               {{...this.input.attributes}}/>
        <p className={`form-group__error`} id={this.error.id}>
          {this.error.text}
        </p>
      </div>,
      this
    );
    
    // Attach validation const mountedInput = document.getElementById(this.input.id);
    const validationLayer = new CustomValidation(mountedInput);
    this.validate = validationLayer.validate; 
  }
}
customElements.define("custom-input", CustomInput);

react/src/app.js

// ...
// all other imports
import "./components/form/input";

// ...
// and other code

 

Leave a Reply

avatar
  Subscribe  
Notify of