DEV Community

Cory LaViska
Cory LaViska

Posted on

On Using Web Component Libraries

We tend to think of components as things that belong to a framework. After all, React has components, Vue has components, Angular has components…it's just how we've always used them.

Because of that, people tend to refer to Lit and FAST Element as frameworks, but they’re not. They’re libraries, and that’s an important distinction.

If you want a React component to work, you have to use it with React. If you want a Vue component to work, you have to use it with Vue. If you want an Angular component to work…well, you get the point.

With web components, the platform is the framework.

Naturally, a follow up question is "why do you need a library then?" The truth is that we don’t. We can create web components without a library. Here's a counter component written in pure JavaScript.

class MyCounter extends HTMLElement {
  static get observedAttributes() {
    return ['count'];
  }

  constructor() {
    super();
    this.state = {
      count: 0
    };
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <button type="button">
        Count:
        <span class="count">${this.state.count}</span>
      </button>
    `;
    this.handleClick = this.handleClick.bind(this);
  }

  connectedCallback() {
    this.shadowRoot.querySelector('button').addEventListener('click', this.handleClick);
  }

  disconnectedCallback() {
    this.shadowRoot.querySelector('button').removeEventListener('click', this.handleClick);
  }

  get count() {
    return this.state.count; 
  }

  set count(newCount) {
    this.state.count = newCount;
    this.update();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'count') {
      this.state.count = Number(newValue);
      this.update();
    }
  }

  handleClick() {
    this.count = this.count + 1;
  }

  update() {
    this.shadowRoot.querySelector('.count').textContent = this.state.count;     
  }
}

customElements.define('my-counter', MyCounter);
Enter fullscreen mode Exit fullscreen mode

We choose to use libraries to improve the the component authoring experience and abstract messy boilerplate into efficient, reusable modules. Here's a functionally equivalent counter built with Lit.

import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-counter')
class MyCounter extends LitElement {
  @property({ type: Number }) count = 0;

  handleClick() {
    this.count++;
  }

  render() {
    return html`
      <button type="button" @click=${this.handleClick}>
        Count: ${this.count}
      </button>
    `;
  }
}
Enter fullscreen mode Exit fullscreen mode

Sure, we can bake features such as declarative rendering and reactivity into each and every component, but that’s not DRY. It would convolute the code and make our components larger and more difficult to maintain. That’s not what I want and it's probably not what my users want.

Alternatively, we could build those features ourselves and split them off into reusable modules — but that's just reinventing the wheel, isn't it?

When you think of it that way, using a library to build web components makes a lot of sense.


Aside: It’s been said that developer experience is the only benefit to using a library. While it’s true that benefits to the end user are marginalized with one-off components, it's worth noting that APIs such as those offered by Lit and FAST Element lead to less bugs due to reduced complexity and less code in the component itself. Consider the counter examples above. Which one is easier to maintain?

This post originally appeared on the author's blog.

Discussion (36)

Collapse
lukeshiru profile image
LUKESHIRU

Here's one written in React:

import { useState } from "react";

const MyCounter = () => {
    const [count, setCount] = useState(0);

    return (
        <button type="button" onClick={() => setCount(count + 1)}>
            Count: {count}
        </button>
    );
};
Enter fullscreen mode Exit fullscreen mode

Here's one written in Svelte:

<script>
    let count = 0;
</script>

<button type="button" on:click={() => count++}>
    Count: {count}
</button>
Enter fullscreen mode Exit fullscreen mode

I assure you, devs don't mind to have React or Svelte if DX looks like this. BTW Svelte can be compiled as Web Component, and for a React like experience with WC you have SkateJS.

Cheers!

Collapse
merri profile image
Vesa Piittinen

For the fun of it, HTML + DOM Level 0!

<button
    type="button"
    data-count="0"
    onclick="this.textContent = 'Count: ' + this.dataset.count++"
>Count: 0</button>
Enter fullscreen mode Exit fullscreen mode

"What could possibly go wrong?"

Or a single source of truth:

<style>[data-count]::after { content: attr(data-count); }</style>
<button type="button" data-count="0" onclick="this.dataset.count++">
    Count:
</button>
Enter fullscreen mode Exit fullscreen mode

Code golf is fun.

Collapse
lukeshiru profile image
LUKESHIRU

Nice example! For the first one you could use template literals as well:

<button
    type="button"
    data-count="0"
    onclick="this.textContent = `Count: ${this.dataset.count++}`"
>Count: 0</button>
Enter fullscreen mode Exit fullscreen mode

And is still kinda better than the second one because afaik ::after has some a11y issues. Obviously the issue with this approach is that is way easier to mess with the "internal" logic/state, because is more exposed, but for the sake of examples it works because is still simpler than Web Components 🤣

Thread Thread
merri profile image
Vesa Piittinen

The a11y issues with pseudo elements might be outdated information, but hard to know for sure since it still is way too often random individuals doing the research.

Thread Thread
lukeshiru profile image
LUKESHIRU

I just tried it with Microsoft Narrator and it read "Count button", I'll try with Voice Over later, but I think this issue still applies.

Collapse
merri profile image
Vesa Piittinen

I feel hacky. But hey, the counter state isn't a global anymore.

I suspect someone has used something like this with their site somewhere and felt like being super clever.

Collapse
darkwiiplayer profile image
DarkWiiPlayer

I assure you, devs don't mind to have React or Svelte

I am currently working on a svelte application and more than once gotten to a point where I feel like I have to fight the framework to represent logic in a readable way (i.e. without having dozens of variables for everything)

I'd much rather write vanilla JS

Collapse
lukeshiru profile image
LUKESHIRU

Can I ask you for an example in which the Web Component version is easier to read/maintain than the Svelte version? I fail to imagine a scenario in which "having dozens of variables" is solved with Web Components instead of being worse.

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer

mostly anything that's more complex than a hello world

Thread Thread
lukeshiru profile image
LUKESHIRU

That doesn't actually answer my question, but ok.

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer

To literally answer your question: You probably can

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer

But no, I actually did answer your question. Most applications that do anything interesting quickly start to look worse when written in svelte than if they were just built with web components.

To illustrate, here's just one example of an advantage: HTMLElement (or, more likely, any boilerplate-reducing wrapper you would use) is is a class, and on top of it one that shares the API of any other HTML element. This means you can use methods like querySelector & co. but you can also implement methods around the specific purpose of the component. Svelte, by contrast, only lets you trigger behaviour by changing state, or awkwardly passing closures around. Neither leads to readable code.

Thread Thread
lukeshiru profile image
LUKESHIRU

Your "example" is kinda awkward. You're just mentioning that custom elements extend HTMLElement, and you can do querySelector, but when you're using tools like Svelte or React you literally don't need to do that kind of stuff. My point asking for an example is that I seriously doubt you can make something simpler with Web Components. The advantage of Web Components is not the simplicity, but the lack of need of extra JS to work (unless you use something like Lit, obviously). You said you can make something simpler with WC compared with Svelte, so that's why I asked for a proper example :/

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer

My whole point is that the problems start to show in more complex examples, and no, I can not send you huge chunks of an internal project from my workplace.

In regards to querySelector, you seem to be missing the point; this isn't about any specific function, it's about custom elements aka. web component sharing most of their API with builtin HTML elements, as well as having methods in the first place.

Thread Thread
tylerlwsmith profile image
Tyler Smith • Edited

Me searching for where LUKESHIRU said "please send me huge chunks of an internal project" Squatting squinting meme

Collapse
shriji profile image
Shriji

You are battling with svelte? Have you seen angular?

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer

Luckily I only know svelte and vanilla JS; although I mostly rely on a few custom helpers to reduce boilerplate when generating HTML from JS because document.createElement is just way too cumbersome. These days I use plain JS to prototype things even if I will later have to translate it into svelte because that's what we use at work.

Thread Thread
dannyengelman profile image
Danny Engelman • Edited

I have done a fair bit with Svelte. Its the fastest REPL around and I like the "Component" paradigm in the .svelte files.
Until my code got too complex.
Svelte (or any Framework) wants you to stick to the paradigm.. its called a Framework for a reason. If you have too much HTML/CSS/JS knowledge you will come to a point where you have to make a decision... stick to the paradigm... or ditch the tool.
I ditched Svelte (apart from the occaisonal quick prototype) because I did not want to be framed.
I ran into problems with Svelte when I wanted to "hotwire" CSS styling and got into a fight with "Svelte"
svelte.dev/repl/382ed83cd7954f6088...
It was a choice... stick to native technology that will work for the next 25 JS years or learn to really use a Framework that will most likely not be around in 25 years time ( oh, I have seen so many technologies come and go since I started in 1994)

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer

Sounds similar to my experience; Svelte wants you to stick to the paradigm, but the paradigm falls apart for more complex applications.

Thread Thread
lukeshiru profile image
LUKESHIRU

In a huge example like the one you shared, where does using Web Components makes the code easier to read/maintain? I keep making that question and the answer I keep getting is that "is complex to do complex things with Svelte", when the thing I want to see is that "complex thing" being simpler in vanilla.

My point, since my first comment, is that tools like Svelte or React makes DX better by making the code easier to read and maintain. Obviously you'll need to load some JS from those tools to make them work, and you'll be doing things differently from how you'll do them with vanilla, but that's exactly the point because vanilla is unnecessarily complex.

Svelte's approach is to let you code "almost vanilla" HTML+CSS+JS (with a similar approach to what Polymer used to do), but it "compiles" your code to be an optimized JS (and the target compilation can be changed to be a WC, SSR, or whatever you need). With Svelte it doesn't make much sense to complain about how it doesn't let you do querySelector of a component, because the idea is to abstract away from methods like that.

React's approach is to abstract away DOM mutations so you can focus on how your components will look like at any point in time, and grabs lots of inspiration from functional programming. With React doesn't make sense to complain of how complex is to set innerHTML, because the idea is to abstract away from smelly code like that.

I know some folks love to do overengineering (because if not then stuff like Angular woundl't exist), but in your "~25 years of experience" are you telling me that you didn't noticed that the vast majority prefers to work with good libraries/frameworks/pre-processors instead of dealing with the complexity underneath until that becomes simple? I get your point that some stuff just stops being useful when the platform becomes good enough, like it happened with jQuery which nowadays doesn't makes much sense, but the thing is for component development, the platform keeps not being "good enough". Similar to how we got fetch because XMLHttpRequest sucked, we need a better API for component development.

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer • Edited

EDIT: I just realised that message wasn't meant in response to me; I answered right from my notifications and had just assumed this was on another comment thread.

In other words, continue reading with the fact in mind that I'm a dumbass; but the points are still valid if applied to that other conversation (and, to some extent, probably to this one too)


I think you're still completely missing my point. I cannot give you a simple example of when complexity becomes too big for svelte to handle nicely. That's the whole point; svelte looks good in almost all simple examples, because there isn't much state to keep around, nor things to do with it. The problem is that the approach svelte takes just doesn't scale well because it is too fine-tuned to hide the complexities of a hello-world example.

And once again, you are totally misunderstanding the querySelector point. It's not about calling querySelector; it's about having a shared API. You can call any method of a normal DOM element on your custom elements, because they are just normal DOM elements. The same doesn't work svelte components, which are a separate object with a horrible API that manages the real DOM nodes.

You also cannot define methods on svelte components, which severely limits the ways you can interact with them. So from the outside, svelte components are ridiculously unwieldy compared to custom elements.

And from the inside, their paradigm for state management just doesn't scale nearly as well as plain javascript. The syntax is awkward and just similar enough to plain JS/HTML that you might get confused (just today a coworker tried doing ${} in the HTML section of a svelte component right after making a similar change in the actual JS part).

But the worst part, by far, is that there is zero re-usability without creating a new component. It's all or nothing. This might work for generic things, say, a component to turn an array into an ordered list; but sometimes these sub-components are simply too tightly linked to their surrounding component to be usable anywhere else, and at that point you're stuck with the boilerplate and performance overhead of a new full component.

And it's not even less boilerplate that you're writing; I've started switching to vanilla JS for prototyping because, aside from having the flexibility to write uglier code as I go, I also just have to write less of it, and on top of that it doesn't even need to be compiled.

I hope that was enough of an explanation. Looking forward to more nagging about how querySelector is bad.

Thread Thread
shriji profile image
Shriji

You know you could use the bind directive in svelte to get the element itself right?

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer

Yes, and you can even do stuff like bind:self={array[index]} to at least save on variables a bit but that doesn't really get you all that far either. In the end it still feels like writing lots of globals, except they're at least scoped to one component.

Thread Thread
shriji profile image
Shriji

In the end it still feels like writing lots of globals.

At this point I don't know whether you are talking about the DOM or the JS itself. Svelte gets as close to JS and I doubt that you are trying to build a frontend or custom elements to be used in another framework.

Collapse
claviska profile image
Cory LaViska Author

Those are great examples if you don't care about lock-in or interoperability. I prefer to use my components in more than one framework — or even without one!

Collapse
lukeshiru profile image
LUKESHIRU

You can produce Web Components with Svelte, but even in vanilla, there are ways of writing less boilerplate:

const MyCounter = () => {
    let count = 0;
    const button = document.createElement("button");
    const update = count => (button.textContent = `Count: ${count}`);

    button.setAttribute("type", "button");
    button.addEventListener("click", () => update(count++));

    update(0);

    return button;
};
Enter fullscreen mode Exit fullscreen mode

DX with vanilla Web Components is really bad, and with Lit (without TS) is not much better:

import { html, css, LitElement } from "lit";

export class MyCounter extends LitElement {
    static properties = {
        count: { type: Number }
    };

    handleClick() {
        this.count++;
    }

    render() {
        return return html`
            <button type="button" @click=${this.handleClick}>
                Count: ${this.count}
            </button>
        `;
    }
}
customElements.define("my-counter", MyCounter);
Enter fullscreen mode Exit fullscreen mode

And you still need to consider that using Lit, you loose something that makes WC valuable that is extending native elements, which isn't supported without a Polyfill in Safari yet (the is property), so your button isn't actually a button, is an element wrapping a button. In React, if I want to send down more properties to the button, I can just do ...props, while working with Web Components I need to declare every property.

Thread Thread
darkwiiplayer profile image
DarkWiiPlayer

while working with Web Components I need to declare every property

That's not entirely true. You only need to declare properties that you want state updates on. If you want to listen on everything (because screw performance, I guess) you can write a 1-liner helper function with a MutationObserver and use it anywhere in your application.

Thread Thread
lukeshiru profile image
LUKESHIRU

Why would I do that having libs like React or Vue that solve that with VDOM, or Svelte without it? I think you might be missing the point x.x

Collapse
dannyengelman profile image
Danny Engelman • Edited

When making a comparison between Web Component Libraries and Native code; then at least make it a fair comparison:

  • No 8 KB Lit library required,
  • No 60 KB React library required,
  • No Built required...
  • Copy/paste in a modern browser, and it just works
  • The final GZip size for this Web Component is 296 Bytes, What does using a Library add?
  • This code will run for the next 25 JavaScript years (remember that Angular 1 to 2 "update"?)

I do like the code-golf examples in this thread; I am sticking with readable Web Component code for the comparison:

  • Don't define separate Classes if you use them only once
  • Chain everything that can be chained in the Constructor
  • Be smart with EventListeners, those good, old, inline handlers are powerful, this.onclick allows a Component user to overload the click event.
  • No need for oldskool bind stuff
  • No need for a connectedCallback, if you only work with shadowDOM (or already parsed global DOM)
  • No need for a disconnectedCallback, any handlers attached to elements inside our Custom Element will be garbage collected
  • DRY will only bloat your delivered code; GZip just loves that this.shadowRoot.querySelector("b").textContent repetition.
  • <count> is an UnknownElement, perfectly valid to use for better semantic code
  • The final GZip size for this Web Component is 296 Bytes, What does using a Library add?

"state" is just a location in memory

Like everyone who has ever done Assembly programming knows

Using the DOM (just a location in memory) as "state" not only shortens the code,
but also make count updates by changing the DOM directly possible.
Something that never works when using Frameworks or Libraries.

You can do a simple experiment with your 5 year old daughter. All you need is a pen and paper.

  • She is the "Web Component", have her write a number on the paper.
  • Now you "Global script" take the pen, strike through her number and write 42 on the paper.
  • Ask her what the number on the paper is.
Collapse
dannyengelman profile image
Danny Engelman

I have added a 3rd term to my "Web Component" Lingo: BaseClass

import { LitElement, html } from 'lit';

html is a library function, LitElement is the BaseClass

Bitten by a Big Vendor 1.0 to 2.0 "upgrade" once; I have written my own BaseClass; I don't ever want to experience that again.

Collapse
stradivario profile image
Kristiqn Tachev • Edited

Hi guys!

You may want to check also this abstraction that i have created based on LitHtml!
github.com/r-html/rhtml/tree/maste...

Functional composition using Partial application combined with typescript decorators :)

You can take a look at this starter github.com/rxdi/starter-client-sid...

Regards!

Collapse
realstandal profile image
Ryan Lockard

React is a library, not a framework - just cause it seems you're implying the contrary. Frameworks make use of the library (Next, Redwood, etc.) ❤️

Collapse
claviska profile image
Cory LaViska Author

They may call it a library, but it serves as a framework for components just like Angular and Vue.

Collapse
realstandal profile image
Ryan Lockard

So the word you're looking for is "paradigm"?

Collapse
tzimpoulog profile image
Tzimpoulas Nikos

StencilJS is a nice alternative

Collapse
claviska profile image
Cory LaViska Author

I wrote some thoughts regarding my experience with Stencil and why I prefer Lit now. abeautifulsite.net/posts/moving-fr...