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.
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