📜 ⬆️ ⬇️

Web components in the real world


Photo by NeONBRAND


Web components are a generic name for a set of technologies designed to help web developers create reusable blocks. The component approach to creating interfaces is well established in front-end frameworks, and it seems like a good idea to embed this functionality natively in browsers. Browser support for this technology has already reached a sufficient level so that you can seriously think about using this technology for your work projects.


In this article we will look at the features of using web components, which evangelists of these technologies for some reason do not speak about.


What are web components?


First you need to decide what exactly is included in the concept of web components. A good description of the technology is on MDN. Briefly, the following features are usually included in this concept:



As an example, write a hello-world component that will greet the user by the name:


 // -     html- class HelloWorld extends HTMLElement { constructor() { super(); //  Shadow DOM this.attachShadow({ mode: "open" }); } connectedCallback() { const name = this.getAttribute("name"); //     Shadow DOM this.shadowRoot.innerHTML = `Hello, <strong>${name}</strong> :)`; } } //     html- window.customElements.define("hello-world", HelloWorld); 

Thus, each time the <hello-world name="%username%"></hello-world> tag is placed on the page, a greeting will appear in its place. Very comfortably!


View this code in action here .


You still need frameworks.


It is widely believed that the implementation of web components will make the framework unnecessary, because the built-in functionality will be enough to create interfaces. However, it is not. Custom html tags really resemble Vue or React components, but this is not enough to replace them entirely. The browsers do not contain anything similar to VDOM - an approach to interface description, when the developer simply describes the desired html, and the framework will take care of updating the DOM – elements that have really changed compared to the previous state. This approach greatly simplifies working with large and complex components, so without VDOM it will be hard.


In addition, in the previous section with an example of a component, you might have noticed that we had to write some amount of code to register the component and activate the Shadow DOM. This code will be repeated in each component created, so it makes sense to bring it to the base class - and now we already have the beginnings of the framework! For more complex components, we will also need a subscription to change attributes, convenient templates, work with events, etc.


In fact, web-based frameworks already exist, for example lit-element . The lit-element already has built-in VDOM, lit-html , and other basic features. Writing components in this way is much more convenient than through the native API.


More often talk about the benefits of web components in the form of reducing the size of the loaded Javascript. However, the lit-element that uses web components weighs 6 kb at best, while there is a preact that uses its components similar to React, but at the same time weighs 2 times less than 3 kb . Thus, the size of the code and the use of web components are orthogonal things and does not contradict one another.


Shadow DOM and performance


Styling large html pages may require a lot of CSS, and it may be difficult to come up with unique class names. Here comes the Shadow DOM. This technology allows you to create areas of isolated CSS. Thus, you can render a component with its own styles that will not overlap with other styles on the page. Even if you have a class name that matches something else, styles will not mix if each one lives in its own Shadow DOM. The Shadow DOM is created by calling the this.attachShadow() method, and then we have to add our styles inside the Shadow DOM, either with the <style></style> or via <link rel="stylesheet"> .


Thus, each component instance receives its own copy of CSS, which obviously should affect performance. This demo shows exactly how. If the render of ordinary elements without Shadow DOM takes about 30 ms, then with Shadow DOM it is about 50 ms. Perhaps in the future, browser manufacturers will improve performance, but now it’s better to abandon small web components and try to make components like <my-list items="myItems"> instead of separate <my-item item="item"> .


It is also worth noting that alternative approaches, such as CSS modules, do not have such problems, because everything happens at the assembly stage, and regular CSS comes to the browser.


Global component names


Each web component is bound to its own tag name using customElements.define . The problem is that the names of the components are declared globally, that is, if someone has already taken the name my-button , you cannot do anything about it. In small projects, where all component names are controlled by you, this is not a particular problem, but if you use a third-party library, then everything can suddenly break when you add a new component with the same name that you already used. Of course, this can be defended by the convention of naming using prefixes, but this approach is very similar to the problems with the names of CSS classes that web components promised us to get rid of.


Tree-shaking


Another problem follows from the global register of components - you do not have a clear connection between the place of registration of the component and its use. For example, in React, any component used must be imported into a module.


 import { Button } from "./button"; //... render() { return <Button>Click me!</Button> } 

We explicitly import the Button component. If you delete the import, then we will have an error in rendering. With web components, the situation is different, we just render html tags, and they magically come to life. A similar example with a button on the lit-element would look like this:


 import '@polymer/paper-button/paper-button.js'; // ... render() { return html`<paper-button>Click me!</paper-button>`; } 

There is no connection between import and use. If we remove the import, but it remains in some other file, the button will continue to work. If the import suddenly disappears from another file too, then only then something will break and it will be very sudden.


The absence of an explicit connection between import and use does not allow tree-shaking of your code, automatic deletion of unused imports. For example, if we import several components, but not all of them, they will be automatically deleted:


 import { Button, Icon } from './components'; //... render() { return <Button>Click me!</Button> } 

Icon in this file is not used and will be safely removed. In the situation with web components, this number will not work, because the bandler is unable to track this connection. The situation is very similar to the year 2010, when we manually connected the necessary jquery plugins into the site header.


Problems with typing


Javascript is inherently a dynamic language, and not everyone likes it. In large projects, developers prefer typing, adding it with Typescript or Flow. These technologies are perfectly integrated with modern frameworks like React, checking the correctness of calling components:


 class Button extends Component<{ text: string }> {} <Button /> // :    text <Button text="Click me" action="test" /> // :   action <Button text="Click me" /> //   ,   

With web components this doesn't work. The previous section explained that the location of a web component is statically unrelated to its use, and for the same reason, Typescript will not be able to display valid values ​​for the web component. Here JSX.IntrinsicElements can come to the JSX.IntrinsicElements - a special interface from where Typescript takes information for native tags. We can add a definition for our button there.


 namespace JSX { interface IntrinsicElements { 'paper-button': { raised: boolean; disabled: boolean; children: string } } } 

Now Typescript will know about the types of our web component, but they are not related to its source code. If new properties are added to the component, in JSX the definition will need to be added manually. In addition, this declaration does not help us in any way when working with an element through a querySelector . There you have to cast the value to the desired type:


 const component = document.querySelector('paper-button') as PaperButton; 

Perhaps, as the standard spreads, Typescript will come up with a way to statically typify web components, but for now, using web components will have to say goodbye to type safety.


Bulk property update


Native browser components, such as <input> or <button> , accept values ​​as text attributes. However, sometimes it may be necessary to transfer more complex data to our components, objects, for example. For this it is proposed to use properties with getters and setters.


 //     DOM const component = document.querySelector("users-list"); //     component.items = myData; 

On the component side, we define the setter that will process this data:


 class UsersList extends HTMLElement { set items(items) { //   this.__items = items; //   this.__render(); } } 

In the lit-element there is a convenient decorator for this - property:


 class UsersList extends HTMLElement { @property() users: User[]; } 

However, it may happen that we need to update several properties at once:


 const component = document.querySelector("users-list"); component.expanded = true; component.items = myData; component.selectedIndex = 3; 

Each setter causes rendering, because it does not know that other properties will be updated there. As a result, we will have two unnecessary updates with which we need to do something. The standard does not provide anything ready, so developers need to wriggle out themselves. In lit-element, this is solved by asynchronous rendering, that is, the setter does not directly cause the update, but leaves a request for deferred rendering, something like setTimeout(() => this.__render(), 0) . This approach allows you to get rid of unnecessary redrawing, but complicates the work with the component, for example, its testing:


 component.items = [{ id: 1, name: "test" }]; //  ,     // expect(component.querySelectorAll(".item")).toHaveLength(1); await delay(); //      expect(component.querySelectorAll(".item")).toHaveLength(1); 

Thus, the implementation of the correct component update is another argument for using the framework instead of working with web components directly.


findings


After reading this article, it may seem that the web components are bad and they have no future. This is not entirely true; they can come in handy in some use cases:



There are also things that I would not do on the web components:



I hope this information will be useful to you when choosing a technology stack this year. I will be glad to hear what you think about it.


')

Source: https://habr.com/ru/post/443032/


All Articles