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:
-
Custom Elements
custom elements allow you to extend HTML and define your own tags. They are considered low level.
-
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 usesawait
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 thecustomElements.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 customHTMLElement
will display itself. Usually you'll also want to pass data to the custom element. Here we decided to name our methodrender(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:
- 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.
- 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
- Developer designer relationship: designers understand graphical output, and Component explorers bridge the gap between code and design.
- Reuse your components: build once use everywhere.
- 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".
- Visual Testing: component explorers allow you to test components visually. paraphrased from HiChroma
How to CDD
-
Install React Storybook if are using react.
npm i -g getstorybook cd app getstorybook
-
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.
-
Then
npm run storybook
-
Navigate to the storybook app to explore your component
-
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).