Web Fundamentals: Web Components Part 1
Tags: webdev, beginners, webcomponents, html
Published: 2023-08-14 10:08:00

---
description: Web components are a web standard, component abstraction toolchain. Let’s take a closer look at them and see how they work under the hood.
---
## Contents

1. Web components are here to stay
2. What are custom elements?
3. Let's build
4. Component lifecycle
5. Summary

## 1. Web components are here to stay

Web components are an abstraction to build and organize UI code. It's a set of browser native tools you can use to encapsulate HTML markup, CSS styles and JavaScript behavior into reusable components [1]. The component model is a useful abstraction when dealing with complex states and non-trivial integrations, and there are many frameworks in this space offering their unique proposition. Web components is one such solution but it's a web standard. The abstraction mainly serves us, the authors of the code, so that we can effectively manage and maintain it in the future. 

The tools the browser provides for this are:

1. Custom elements
2. Shadow DOM
3. HTML templates

In this post, we'll be taking a closer look at what custom elements are, how to create them and how they work under the hood.

## 2. What are custom elements?

Custom elements enables you to define your own HTML tags, and encapsulate markup and behavior within it. For example, if you're building a counter component, you can use a custom element to keep track of the count and the click listeners for the buttons within it and also give it a meaningful name. By naming it in the tag, as opposed to giving an existing HTML element like a `div` an ID, you define one place where logic related to that component should live. It'll roughly behave the same way, but it has better semantics at the HTML level.

![Two HTML counter components with a decrement and increment button along with a span showing the current count. The first snippet shows this component implemented with a wrapping `div` and the second shows the same but with a custom HTML tag called `custom-counter`.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y4otfu5pqocgoyz7pqgc.png)

The first approach is perfectly valid and for a simple component like a counter, this would suffice. It also requires fewer lines of JavaScript than the web component counterpart, however, when the complexity of the component grows, the additional lines of JavaScript are warranted. The grouping of related code together also reduces a part of the maintenance burden, which is to hunt down all the different parts of your codebase that could be managing this component's behavior.

What tools do we need to build custom elements? Since web components are native to the web platform, you don't need any additional dependencies to get you going. There are frameworks that help remove some boilerplate but, in essence, the workflow to build your own custom element is to create a class that extends `HTMLElement` and pass that as a parameter to a built-in API `customElements.define` with a tag name. Easy!

## 2. Let's build

The best way to learn is by doing, so let's start building and explore any theory along the way. To follow along, you'll only need a text editor and a browser. You might notice from the code snippets or the source code for this post that I'm using Astro and TypeScript but you can just as easily follow along with plain HTML and JavaScript. When doing so, open your HTML file directly in your browser to see your page in action, or if you really want to serve it from a static file server, I recommend the CLI tool `serve` [2]. Using that tool is as simple as:

```sh
serve <your-file>.html
```

In this post, we'll be building a timer component and it's going to look like this:

![An Excalidraw mockup of a timer component. The component is shown with an initial count of 0 and defined with HTML at the top and below is the same component after one second with the count reading 1. Between the two components is some orange text that reads "Increment count every second". The markup of the component is `<article><h2>Timer</h2><div id="timer"><p>Count: <span>0</span></p></div></article>`.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wk3z6qfpwrtqgwpwqhni.png)

As soon as the page loads, the counter is going to start incrementing every second. However, instead of a `div` with a "timer" ID, we're going to make our own web component called "x-timer" (I wanted to call it "timer" but one of the rules of custom elements is that it needs to be at least two words separated by a hyphen, so you can distinguish them from built-in HTML elements [3] and also naming things is hard). That will make the final markup look like this:

![An Excalidraw diagram of the markup of the `x-timer` component. The markup reads: `<article><h2>Timer</h2><x-timer><p>Count: <span>0</span></p></x-timer></article>`.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/47eqsmmjh4na7r8fghx4.png)

We want the component to have the following internal markup so we can update just the `span` with the incrementing count values:

```html
<p>
  Count: <span>0</span>
</p>
```

Before we start coding it, let's establish the three ways you can define this internal markup:

1. Define all of the markup in HTML, and use JavaScript to hook into `x-timer`'s children elements
2. Use JavaScript to create HTML elements and then add it to the inside of `x-timer` as children elements
3. A combination of the first two approaches

For our `x-timer` component, we're going to define the web component itself in the markup and then use JavaScript to define its internals:

```html
<article>
  <h2>A single timer</h2>
  <x-timer />
</article>
```
> _**Correction:**_ This is incorrect HTML and can lead to unexpected results. The correct way to denote an empty HTML element is by using both the opening and closing tags: `<x-timer></x-timer>`. For more context, see [Web Fundamentals: The HTML-JSX Confusion](https://dev.to/hasanhaja/web-fundamentals-the-html-jsx-confusion-438p).

In our JavaScript, we can define the component such that when it's loaded into the document, it'll create the internal HTML elements and expand into this:

```html
<article>
  <h2>A single timer</h2>
  <x-timer>
    <p>Count: <span>0</span></p>
  </x-timer>
</article>
```

Awesome! This gives us some scaffolding and a plan of action.

We'll need the following structure for any web component we want to define:

```typescript
class Timer extends HTMLElement {
  constructor() {
    super();
  }
}

customElements.define("x-timer", Timer);
```

We've named our class `Timer` and that extends an `HTMLElement`. We want our custom tag to be called `x-timer` and for the browser to recognize it, we need to define it using `customElements.define` and give it a class so it knows what to do for that tag. Now, let's make it dance:

```typescript
class Timer extends HTMLElement {
  count: number;

  constructor() {
    super();
    this.count = 0;
    
    setInterval(() => {
      console.log("Timer called");

      this.count++; 
    }, 1000);
  }
}

/* --snip -- */
```

We've added a property to the class to keep track of the count state, and initialized it with 0 when it's constructed. We also defined a timer using the JavaScript function `setInterval` and passed in a delay of 1000 milliseconds, along with a callback that both logs to the console every time the timer is called and increments the `count` property of the class by 1. This is the entire mechanism of the component that will drive timer, but we still need to put this count on the screen.

```typescript
class Timer extends HTMLElement {
  /* --snip -- */
  constructor() {
    /* --snip -- */
    const countSpan = document.createElement("span");
    const countParagraph = document.createElement("p");
    countParagraph.textContent = "Count: ";
    countSpan.textContent = this.count.toString();

    countParagraph.appendChild(countSpan);
    this.appendChild(countParagraph);
    
    setInterval(() => {
      /* --snip -- */
      countSpan.textContent = this.count.toString();
    }, 1000);
  }
}

/* --snip -- */
```

That should do it! We've defined a `span` and a `p` within the constructor and then set their initial text content. We've then appended the span into the paragraph, and finally the paragraph into the parent (our `x-timer` component), to create the final structure of our component. In the `setInterval` callback, we've added a line to update the span's text content whenever count changes. When you view this in your browser, you should see counter incrementing every second like we set out to do. Adding styles is optional, but you should now have a page that roughly resembles this:

![A screenshot of the timer component on the screen with a heading "Web components demo" with the browser console open on the right.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9zcdjmw9fv7rt96qbwpi.png)

We're not quite done because I've misled you a bit and shown you code that can break unexpectedly. While this happens to work when we've explicitly used this custom element in our HTML, it'll break in weird ways when you create it dynamically like this and try to insert it into the document:

```typescript
const timer = document.createElement("x-timer");
document.body.appendChild(timer);
```

We'll get weird errors like this:

```sh
Uncaught DOMException: Operation is not supported
```

To understand what is going on here, we need to get introduced to the lifecycle of components.

## 3. Component lifecycle

How does the browser render a web component? How does it update when attributes change or the component gets removed from the document? So far in our implementation, we've assumed the browser recognizing a web component and rendering it onto the screen to be the same event. However, they are distinct events in the browser's rendering pipeline and this has an impact on how we define and manage the components. 

The browser's rendering pipeline can be generalized into three phases [4]:

1. HTML parsing
2. Calculating layouts and calculating painting details
3. Compositing all of the individual elements together and finally draw them on the screen

![An Excalidraw diagram visualizing how and when the browser recognizes and renders web components. When the browser encounters the web component in the parsing phase, it calls the constructor to instantiate it, and continues parsing the rest of the component. Only when it reaches the layout and painting phase does it invoke the lifecycle methods, and in this phase the components have access to DOM operations.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fodi0e2ujfcq0bim52a0.png)

When the browser encounters a web component, it instantiates it by calling the constructor and continues parsing the rest of the document. Since it's not yet being rendered, it won't have access to the document. This is why our components behavior is flaky depending on if we use it directly or create it programmatically. The way to properly build our component would be to hook into what are called "lifecycle methods". These are methods on the component that get invoked later in the rendering pipeline when DOM operations are available. These methods are [3]:

1. `connectedCallback`
2. `disconnectedCallback`
3. `attributeChangedCallback`
4. `adoptedCallback`

These callbacks are invoked in the Layout and Painting phase. Let's refactor our implementation to make use of the `connectedCallback`:

```typescript
class Timer extends HTMLElement {
  /* --snip -- */
  countSpan: HTMLElement;

  constructor() {
    /* --snip -- */
    this.countSpan = document.createElement("span");
    
    setInterval(() => {
      /* --snip -- */
      this.countSpan.textContent = this.count.toString();
    }, 1000);
  }

  connectedCallback() {
    console.log("x-timer connected");

    const countParagraph = document.createElement("p");
    countParagraph.textContent = "Count: ";
    this.countSpan.textContent = this.count.toString();

    countParagraph.appendChild(this.countSpan);
    this.appendChild(countParagraph);
  }
}

/* --snip -- */
```

We've moved all the logic related to appending elements to the web component to the `connectedCallback` method. We've also had to make `countSpan` a property of the component so that we can reference it outside the constructor's scope. We can happily initialize it in the constructor because we're just creating an element and not using it in any DOM operation until it's connected to the document. If you refresh your browser with those changes, you'll see it still works the same but it's been implemented correctly so that it can even be created programmatically without any hiccups.

We're not quite in the clear though. What happens when our component is removed from the document? By looking at the names of our lifecycle methods, it appears that the `disconnectedCallback` method will get invoked, but do we need to do anything with it? In our implementation, our component will only get disconnected when the entire page is disconnected, either through navigation or page close. In that case, we don't need to worry about manually disconnecting or deleting anything, and we can rely on the browser to perform the cleanup actions. We only need to worry about it if we remove web components dynamically, but that shouldn't be something we stipulate in our component design. We should build our components so that it can be used just like any other HTML element. So, what part of our component requires manual cleanup? The dynamically created paragraphs and spans will get removed automatically when the parent component gets removed. And so will the component's properties. The browser's garbage collector will handle all of that like it does for any JavaScript object. What about our timer?

`setInterval` works by taking in a callback function and a delay. It then executes that callback repeatedly with the delay we defined between each execution. However, where does this interval live in memory? Is it scoped to the context it was created? So in our case, would that be the constructor or the class? The short answer is that it isn't scoped to the context it was created in and has a separate execution context altogether.

![The function signature of `setInterval` expanded into words like this: "setInterval(any function we want to execute, delay in milliseconds);".](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yh4bnzbzighsfcgpfkk2.png)

I like to think of the interval as living in some global context and the `setInterval` function gives us a way to push things onto it (for a deeper look into how it works, see this article on the [JavaScript Event Loop](https://dev.to/ocodista/javascript-event-loop-breaking-down-the-mystery-2c9f)). This means that the interval we created will outlive the component by default because the global context will outlive the component. This is precisely why the full signature of `setInterval` also returns an ID that we can use to delete the interval using the method `clearInterval`.

![The full signature of `setInterval` showing that it returns an ID. It reads `setInterval(callback, delay) => id`.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aupjj7wiq7w86tdofg8h.png)

If we don't delete the interval, it will continue to execute in the background and can potentially lead to memory leaks. The browser's garbage collection will kick in when you navigate to a different page or close the tab, but if you're intention is to build a long-lived application then this will accumulate indefinitely and bring the application to a grinding halt. We need to refactor our code slightly to account for the cleanup of intervals:

```typescript
class Timer extends HTMLElement {
  /* --snip -- */
  timerId: number;

  constructor() {
    /* --snip -- */
    this.timerId = setInterval(/* --snip -- */);
  }
	
  /* --snip -- */

  disconnectedCallback() {
    console.log("x-timer disconnected");

    clearInterval(this.timerId);
  } 
}

/* --snip -- */
```

We've assigned the `timerId` returned by `setInterval` to a property so we can reference it outside of the constructor’s scope, and we clear the interval in the `disconnectedCallback`. If we re-run our page, everything should look... the same. Well, that's a little anti-climatic.

Let's create a completely new component whose sole responsibility it is to add new timers and clear all of them when we're done. We can use this new component to tinker with `x-timer`:

![An Excalidraw mockup of the new component. It has the title "Dynamic timers" and two buttons below it that says "Add timer" and "Clear timers". Below it is the text "No timers running". On the right of the mockup, is the markup of the element, which reads `<article><h2>Dynamic timers</h2><x-timers><button>Add timer</button><button>Clear timers</button><div id="timers" /></x-timers></article>`.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qor50870fyng92akaakw.png)

To changes things up a little, let's define most of the internals of the component using HTML and so we can see what it looks like to use JavaScript to hook into children elements. The only element we'll be adding dynamically is `x-timer`, so I've added an empty `div` to be a container for all of them. That would make our HTML look like this:

```html
<article>
  <h2>Dynamic timers</h2>
  <x-timers>
    <button aria-label="add-timer">Add timer</button>
    <button aria-label="clear-timers">Clear timers</button>
    <div aria-label="timers">
    </div>
  </x-timers>
</article>
```

> **Note:** I've made use of `aria-label` to demonstrate different ways of selecting an element in the document, but you can select them however you'd like.

Then the corresponding component definition would look like this:

```typescript
class Timers extends HTMLElement {
  constructor() {
    super();
  }
	
  connectedCallback() {
    console.log("x-timers connected");
    const timersDiv = this.querySelector("[aria-label='timers']");
    const addTimerButton = this.querySelector("[aria-label='add-timer']");
    const clearTimersButton = this.querySelector("[aria-label='clear-timers']");

    // Default state
    timersDiv.textContent = "No timers running";

    clearTimersButton.addEventListener("click", () => {
      // Return to default state
      timersDiv.textContent = "No timers running";
    });

    addTimerButton.addEventListener("click", () => {
      // Clear timers div if no timers present
      if (timersDiv.textContent === "No timers running") {
        timersDiv.textContent = null;
      }
      const xTimer = document.createElement("x-timer");
      timersDiv.prepend(xTimer);
    });
  }
}

customElements.define("x-timers", Timers);
```

Since there was no setup required for this component, all of the logic lives in the `connectedCallback`. You should now have a page that roughly resembles this:

![A screenshot of the final application. There are two main elements on the screen. The first one is a single timer and the second is the dynamic timer component with no timers currently running.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ategctkk3mz7771e32xa.png)

You can create as many timers as you'd wish and clear them and since we've added the appropriate cleanup logic to `x-timer` everything will work correctly. Now, you can experiment with what happens when you remove `clearInterval` from `disconnectedCallback` in `x-timer`. Since we've added some helpful logging statements, you should be able to see when the different methods get called.

![A screenshot of the application running with the browser's console open to show the logging added to the web components.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ww11xqtj1rrp0n8953ks.png)

## Summary

Custom elements are a key part of the web component's toolchain. They enable you to define your own HTML tag and also let you define the inner workings of your component. By looking at how the browser parses and renders HTML, we saw why we need component lifecycle methods and when they get invoked. We leveraged it to ensure we cleaned up after ourselves so we don't create a memory leak in our application. All of this sets the stage for us to explore just how powerful and capable the web component toolchain is.

In the next part, we'll build on the lifecycle methods and in Part 3, we'll explore the Shadow DOM and HTML Templates. We'll also explore web components’ place in the web UI ecosystem alongside giants like React, Angular and Vue. We’ll approach it as objectively as possible to understand the real, non-negligible tradeoffs you make with each approach. The aim is to give you the tools and context so you can make the right calls for your problems.

All the source code for this post can also be found on my [GitHub](https://github.com/hasanhaja/web-fundamentals/tree/main/packages/web-components). You might even get a little sneak peek at the demos and comparisons I’m putting together for Part 2 👀

If you think of anything I've missed or just wanted to get in touch, you can reach me through a comment, via [Mastodon](https://fosstodon.org/@hasanhaja), via [Threads](https://www.threads.net/@hasan.haja), via [Twitter](https://twitter.com/hasanhaja) or through [LinkedIn](https://www.linkedin.com/in/hasan-ali-0b247310b/).

## References
1. MDN Web Components [[Website](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components)]
2. serve CLI tool [[NPM](https://www.npmjs.com/package/serve)]
3. Using Custom Elements [[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)]
4. Overview of RenderingNG architecture [[Website](https://developer.chrome.com/articles/renderingng-architecture/)]