DEV Community

Rich Harris
Rich Harris

Posted on • Updated on

Why I don't use web components

For my first post on dev.to I thought I'd write about a nice, safe topic that's free of controversy: web components.

I'm mostly writing this for my future self, so that I have something to point to next time someone asks why I'm a web component skeptic, and why Svelte doesn't compile to custom elements by default. (It can compile to CEs, and it can consume CEs as evidenced by its perfect score on Custom Elements Everywhere.)

None of this should be taken as criticism of the hard work that has been done on web components. It's possible that I have made some errors in this post, in which case I'd welcome corrections.

Nor am I saying that you shouldn't use web components. They do have valid use cases. I'm just explaining why I don't.

1. Progressive enhancement

This may be an increasingly old-fashioned view, but I think that websites should work without JavaScript wherever possible. Web components don't.

That's fine for things that are intrinsically interactive, like a custom form element (<cool-datepicker>), but it's not fine for your nav bar. Or consider a simple <twitter-share> element that encapsulates all the logic for constructing a Twitter web intent URL. I could build it in Svelte and it would generate server-rendered HTML like this:

<a target="_blank" noreferrer href="..." class="svelte-1jnfxx">
  Tweet this
</a>

In other words, a bog-standard <a> element, in all its accessible glory.

With JavaScript enabled, it progressively enhances — rather than opening a new tab, it opens a small popup window instead. But without, it still works fine.

By contrast, the web component HTML would look something like this...

<twitter-share text="..." url="..." via="..."/>

...which is useless and inaccessible, if JS is disabled or somehow broken, or the user is on an older browser.

The class="svelte-1jnfxx" is what enables encapsulated styles without Shadow DOM. Which brings me onto my next point:

2. CSS in, err... JS

If you want to use Shadow DOM for style encapsulation, you have to include your CSS in a <style> element. The only practical way to do so, at least if you want to avoid FOUC, is to have the CSS in a string in the JavaScript module that defines the custom element.

This runs counter to the performance advice we've been given, which can be summarised as 'less JavaScript, please'. The CSS-in-JS community in particular has been criticised for not putting CSS in .css files, and yet here we are.

In future, we may be able to use CSS Modules alongside Constructable Stylesheets to solve this problem. And we may be able to use ::theme and ::part to style things inside Shadow DOM. But these aren't free of problems either.

3. Platform fatigue

At the time of writing, there are 61,000 open issues on https://crbug.com, the Chromium bug tracker, which reflects the enormous complexity of building a modern web browser.

Every time we add a new feature to the platform, we increase that complexity — creating new surface area for bugs, and making it less and less likely that a new competitor to Chromium could ever emerge.

It also creates complexity for developers, who are encouraged to learn these new features (some of which, like HTML Imports or the original Custom Elements spec, never catch on outside Google and end up being removed again.)

4. Polyfills

It doesn't help that you need to use polyfills if you want to support all browsers. It really doesn't help that the literature on Constructable Stylesheets, written by a Googler (hi Jason!), doesn't mention that they're a Chrome-only feature (edit: this has been fixed after I opened a pull request). The three spec editors are all Googlers. Webkit seem to have some doubts about some aspects of the design.

5. Composition

It's useful for a component to be able to control when (or whether) its slotted content is rendered. Suppose we wanted to use the <html-include> element to show some documentation from the network when it became visible:

<p>Toggle the section for more info:</p>
<toggled-section>
  <html-include src="./more-info.html"/>
</toggled-section>

Surprise! Even though you didn't toggle the section open yet, the browser already requested more-info.html, along with whatever images and other resources it links to.

That's because slotted content renders eagerly in custom elements. It turns out that most of the time you want slotted content to render lazily. Svelte v2 adopted the eager model in order to align with web standards, and it turned out to be a major source of frustration — we couldn't create an equivalent to React Router, for example. In Svelte v3 we abandoned the custom element composition model and never looked back.

Unfortunately this is just a fundamental characteristic of the DOM. Which brings us to...

6. Confusion between props and attributes

Props and attributes are basically the same thing, right?

const button = document.createElement('button');

button.hasAttribute('disabled'); // false
button.disabled = true;
button.hasAttribute('disabled'); // true

button.removeAttribute('disabled');
button.disabled; // false

I mean, almost:

typeof button.disabled; // 'boolean'
typeof button.getAttribute('disabled'); // 'object'

button.disabled = true;
typeof button.getAttribute('disabled'); // 'string'

And then there are the names that don't match...

div = document.createElement('div');

div.setAttribute('class', 'one');
div.className; // 'one'

div.className = 'two';
div.getAttribute('class'); // 'two'

...and the ones that just don't seem to correspond at all:

input = document.createElement('input');

input.getAttribute('value'); // null
input.value = 'one';
input.getAttribute('value'); // null

input.setAttribute('value', 'two');
input.value; // 'one'

But we can live with those quirks, because of course some things will be lost in translation between a string format (HTML) and the DOM. There's a finite number of them, and they're documented, so at least you can learn about them given enough time and patience.

Web components change that. Not only are there no longer any guarantees about the relationship between attributes and props, but as a web component author, you're (presumably?) supposed to support both. Which means you see this sort of thing:

class MyThing extends HTMLElement {
  static get observedAttributes() {
    return ['foo', 'bar', 'baz'];
  }

  get foo() {
    return this.getAttribute('foo');
  }

  set foo(value) {
    this.setAttribute('foo', value);
  }

  get bar() {
    return this.getAttribute('bar');
  }

  set bar(value) {
    this.setAttribute('bar', value);
  }

  get baz() {
    return this.hasAttribute('baz');
  }

  set baz(value) {
    if (value) {
      this.setAttribute('baz', '');
    } else {
      this.removeAttribute('baz');
    }
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'foo') {
      // ...
    }

    if (name === 'bar') {
      // ...
    }

    if (name === 'baz') {
      // ...
    }
  }
}

Sometimes you see things go the other way — attributeChangedCallback invoking the property accessors instead. Either way, the ergonomics are disastrous.

Frameworks, by contrast, have a simple and unambiguous way to pass data into a component.

7. Leaky design

This point is a bit more nebulous, but it weirds me out that attributeChangedCallback is just a method on the element instance. You can literally do this:

const element = document.querySelector('my-thing');
element.attributeChangedCallback('w', 't', 'f');

No attribute changed, but it will behave as though it did. Of course, JavaScript has always provided plenty of opportunities for mischief, but when I see implementation details poke through like that I always feel as though they're trying to tell us that the design isn't quite right.

8. The DOM is bad

Ok, we've already established that the DOM is bad. But it's hard to overstate what an awkward interface it is for building interactive applications.

A couple of months back, I wrote an article called Write less code, intended to illustrate how Svelte allows you to build components more efficiently than frameworks like React and Vue. But I didn't compare it against the DOM. I should have.

To recap, here's a simple <Adder a={1} b={2}/> component:

<script>
  export let a;
  export let b;
</script>

<input type="number" bind:value={a}>
<input type="number" bind:value={b}>

<p>{a} + {b} = {a + b}</p>

That's the whole thing. Now, let's build the same thing as a web component:

class Adder extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: 'open' });

    this.shadowRoot.innerHTML = `
      <input type="number">
      <input type="number">
      <p></p>
    `;

    this.inputs = this.shadowRoot.querySelectorAll('input');
    this.p = this.shadowRoot.querySelector('p');

    this.update();

    this.inputs[0].addEventListener('input', e => {
      this.a = +e.target.value;
    });

    this.inputs[1].addEventListener('input', e => {
      this.b = +e.target.value;
    });
  }

  static get observedAttributes() {
    return ['a', 'b'];
  }

  get a() {
    return +this.getAttribute('a');
  }

  set a(value) {
    this.setAttribute('a', value);
  }

  get b() {
    return +this.getAttribute('b');
  }

  set b(value) {
    this.setAttribute('b', value);
  }

  attributeChangedCallback() {
    this.update();
  }

  update() {
    this.inputs[0].value = this.a;
    this.inputs[1].value = this.b;

    this.p.textContent = `${this.a} + ${this.b} = ${this.a + this.b}`;
  }
}

customElements.define('my-adder', Adder);

Yeah.

Note also that if you change a and b in the same instant, it will result in two separate updates. Frameworks don't generally suffer from this issue.

9. Global namespace

We don't need to dwell on this one too much; suffice it to say that the dangers of having a single shared namespace have been well understood for some time.

10. These are all solved problems

The biggest frustration of all is that we already have really good component models. We're still learning, but the basic problem — keep the view in sync with some state by manipulating the DOM in a component-oriented fashion — has been solved for years.

Yet we're adding new features to the platform just to bring web components to parity with what we can already do in userland.

Given finite resources, time spent on one task means time not spent on another task. Considerable energy has been expended on web components despite a largely indifferent developer population. What could the web have achieved if that energy had been spent elsewhere?

Latest comments (119)

Collapse
 
devidmaul9990 profile image
Devidmaul

It's crucial to respect personal preferences, yet consider embracing progress. Web components offer modular, reusable code, enhancing scalability and maintainability. Exploring their benefits might surprise you, opening new doors to efficient development. Keeping an open mind can lead to valuable insights and improved workflows. Strands Game

Collapse
 
noelkara125 profile image
noelkara125

main gacor terus di sini linklist.bio/kancilbola

Collapse
 
chmich profile image
Christian Sedlmair • Edited

I had to use Svelte in Web-Components.
Reason is the initialization in a Rails-7 App, with Hotwired/Turbo:

On Turbo, Page is loaded once and by changing pages there is only changed the body, not the whole page.

So, a Svelte App inside a Web-Component is build once if the app loads and independend if the initialized Component is present in the first view. You need no further initialization event and if you visit a page where the component is called, its just there!

In case of building svelte in a regular component you have to run a initialization event on every page visit. Then you always see a unpleasent «blink»: All HTML which comes from backend appears instantly while the svelte components needing some milliseconds for building and that looks really bad.

Impediment was the shadow-dom because i want to use global stlyes, and, unfortunately building svelte components on custom-elements by sveltes default functions, build this silly shadow-dom. That was solved by npm/svelte-tag

Collapse
 
dasun profile image
Dasun Sameera Weerasinghe

a spam

Collapse
 
anuragvohraec profile image
Anurag Vohra

none of the problems you mentioned is are any serious threat, to be honest.
I use lit-html to create templates and my own custom library bloc-them (based on web components). And none of the problems you raising alarm for has botthered me so far!

Collapse
 
michaelangelozzi profile image
run_the_race
  1. I wish browsers would also implement a feature to disable CSS, and HTML. That way in addition to being able to turn off interaction on interactive sites, we could turn off styling on styled sites, and turn off content on sites that have content. When I read a magazine that has too many pictures, I stop reading it and request the writer to spend time making version that suites me better.

  2. I create a stylesheet for each component, and then it links to its own stylesheet.

  3. I agree browsers are complex, and unfortuantely bloating due to thier history, but web components are definately needed in the modern web. There are other areas where maybe things could be pruned.

  4. Full evergreen browser support at the moment.

  5. Thats you job to write the code that controls when items are loaded. Call me old fashioned, but when I add items to the DOM, I expect them to be added.

  6. If people didnt have the freedom to reflect properties and attributes the way they require, others would complain about the lack of freedom. Others complains about how they have to implement the details. Write a base class that implement the refelection they way you like, and enjoy the freedom.

  7. Maybe rename point 7 to: Why I don't use call backs, because it could be called by something else other than the function I passed it to. PS, I think you will hate Python which is a language for consenting adults, this allows one to write really elegant solutions, instead of treating you like a child.

Collapse
 
cstolli profile image
Stolli

This article is terrible! OP please just delete this nonsense.

Collapse
 
arthur40056743 profile image
Arthur

great post

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀 • Edited

A nice read! Wish I was a googler, I think I have discovered a way to handle FOUK in web components, I wrote a tiny library around it.
npmjs.com/package/reactive-css-pro...

The library needs a little fix-up 🙄, it's on my to-do list, anyway the idea is kind of a better version of css in js, predifine css custom properties in the document, then each web-component takes ownership of its own css property having for lack of a better word, data transfered them to the :host. The result is no FOUK.

Collapse
 
sinandev profile image
Sinan

You have to think different with web components. Most of your points are not true, while I can agree on others. I think web components will replace fronted frameworks in the future and I don't think there is anyone online with JS disabled, nearly every site uses JS.

Also web components are amazing with tailwind css btw.

Collapse
 
kayodebristol profile image
Kayode Bristol

This article has seemed to cause a defensive reaction in the comments and in me as well (painful to admit).

For me, that defensive reaction was caused by my belief that I needed to use web components. I thought and assumed that there was no easy way to integrate with other frameworks if I didn't.

My suggestion for an alternative title for this piece would be, "Why you don't need to use web components and perhaps shouldn't."

In essence, what I've taken away from the experience of reading the article and comments is that web components aren't a go-to thing. They are a have-to thing. But if I'm using svelte (which I love btw), then I should seriously question my assumptions before setting customElement: true compiler flag.

Collapse
 
elgs profile image
Qian Chen • Edited

When you said DOM was bad, you were saying C/C++ is bad, and Python is good. The frameworks that made you feel good are the ones that some smart people work hard to share their best/opinionated practices. Unless you don't intend to be serious, you have no way to avoid DOM.

Collapse
 
meanstudio profile image
mean.studio

Rich, I can see that you are master salesmen. Comparing worst practices of competitor with best practices of yourself. Not fair.

Collapse
 
geraalcantara profile image
Gerardo Alcantara

Why is not fair ? Can you elaborate on this more precisely?

Collapse
 
ozanmuyes profile image
Ozan Müyesseroğlu

We might as well go back to papyrus, or better yet stones, to show something to the end-user, as they are rock-solid.

Collapse
 
jimmont profile image
Jim Montgomery • Edited
  1. Progressive enhancement
    (any-thing tag)fall-back where needed(/any-thing tag)
    use slots, etc for your range of business requirements

  2. Css in, err... JS
    FOUC is simply not true unless a solution did not deliver a working solution that addresses this specifically. Consider continuing to become familiar with the technology in the context of an actual business case. There are various ways to manage these issues, which are more relevant to actual solutions. If the target audience is so far legacy that the solution should use HTML without anything modern, including JavaScript, then by all means forego what is both contemporary and a long-term future for the web platform. However it calls into question why the article was written or relevant more generally.

  3. Platform fatigue
    Actual use of native Web Components does the opposite: reduces the body of knowledge to maintain and allows a focus on fundamentals. Further the platform is becoming more stable over time (years), not less. Simply look at the information for each browser as well as the MDN docs for any given specific or the platform overall.

  4. Polyfills
    I don't understand how using a polyfill is a problem to achieve parity with any framework, feature or otherwise in any actual project. Yes APIs change over time, they evolve, and are needed less and less over time. That's not a case against. There are iterations of React, Angular, etc. If anything it takes all the various moving targets and focuses them, resulting in less work. Less work is good.

  5. Composition
    Consider using a well designed and implemented component rather than a poorly implemented or designed component. It would lead to (at least in my experience) a better solution, less work which I personally see as desirable.

  6. Confusion between props and attributes
    No properties and attributes are not the same. This has been true a long time and am glad we can together discover the technology we're working with. If anything using native technology like Web Components helps address these common deficits and lack of knowledge. This is a helpful exercise toward discovery.

  7. Leaky design
    I don't understand how something "nebulous" or "weird" equates to "leaky". Help me out. It's a callback, not a setter. Please use the features and read the documentation (just Google: MDN attributeChangedCallback).

  8. The DOM is bad
    No the DOM is not bad, it's good and useful and performant. We should all use it directly instead of writing wrapping APIs that do it for us instead, at least until we know what we're doing. Not all the APIs are well designed or necessarily obvious without some work. One of the great thing about native technologies is that they have a more rigorous implementation path with higher standards and experience than simply: is good or is bad. This reveals flawed knowledge across this space. We all have things to learn, I certainly do.

  9. Global namespace
    So? The registry, parsing, etc are prior work and are entirely acceptable. We don't have some long-hand method for element instantiation: new window.EventTarget.Node.Element.HTMLElement.HTMLDivElement(). Custom elements continue all these patterns--it's how they're supposed to work.

  10. These are all solved problems
    Fine. If you don't want the modern iteration of a solution that's totally OK. Enjoy maintaining the various legacy abstractions, build processes perhaps, etc that come with that work.