DEV Community

Cory LaViska
Cory LaViska

Posted on • Updated on • Originally published at abeautifulsite.net

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?

Top comments (27)

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
 
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
𒎏Wii 🏳️‍⚧️

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
 
shriji profile image
Shriji

You are battling with svelte? Have you seen angular?

Thread Thread
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

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
𒎏Wii 🏳️‍⚧️

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

 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️ • 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
𒎏Wii 🏳️‍⚧️

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.

 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

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
 
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
 
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.
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

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.

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
 
claviska profile image
Cory LaViska

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

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"?