Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

What do you not like about WebComponents?

Say we want to make an icon that when clicked shows how often it was clicked. The webcomponent code seens quite sane to me:

   class HelloIcon extends HTMLElement {
      connectedCallback() {
        this.clickCount = 0;

        this.innerHTML = `
          <button class="icon">:)</button>
          <dialog>
            <p>Hello, I was clicked 0 times</p>
            <button class="close">Close</button>
          </dialog>
        `;

        const icon = this.querySelector('.icon');
        const dialog = this.querySelector('dialog');
        const closeBtn = this.querySelector('.close');
        const dialogText = this.querySelector('p');

        icon.addEventListener('click', () => {
          this.clickCount++;
          dialogText.textContent = `Hello, I was clicked ${this.clickCount} times`;
          dialog.showModal();
        });

        closeBtn.addEventListener('click', () => dialog.close());
      }
    }
Try it here:

https://plnkr.co/edit/0XUOLyM52xfFiIBu?open=index.html&previ...

 help



I don't know if that's idiomatic webcomponent or not but that looks unmaintainable, even for such a minimal component, imagine once it grows. It's not separating presentation from state and logic. querySelector need to match classes and elements in the markdown. Mutating textContent is going to run out of sync as soon as you have more than one event source doing the same. Those are all problems that React solve by separating state and only rendering in one path.

That's the best case scenario and already looks like crap. If that looks sane to you, I don't know what to say.

Make it have an initial count via attributes, sync it with the DOM so if the attribute changes the count resets, and make the JS property always match the attribute (and vice-versa) so it behaves sanely. You're in for a world of pain even for something as simple as this.

Plus they're not declarative: you will only make me use innerText-based updates by threatening me and my family.

Plus they only work with JS enabled, while I can use JSX in SSR.

I've worked extensively with Web Components. They suck.


Web components are intentionally low-level, they’re not really meant to be used directly. You should see them more as performant, interoperable building blocks. Have you tried lit?

This circles back to the OP's point: they're not appropriate to use directly, so we layer a framework on top of them (Lit), and now we're just using another framework with one small piece swapped out.

Except for the hyphens, I don't feel any closer to the platform when I use Lit.


You can inspect a Lit component directly in Chrome, it’s just a custom element. No need for a browser dev extension. You can use this custom element from React or any other JS framework. You can also use this custom element in a server-rendered template, no need for any custom SSR setup. Feel close to the platform yet?

Here's equivalent svelve code. I can't format it correctly because I'm on mobile but even without proper indentation it's easy to read.

<script> let count = $state(0); let dialog; </script>

<button onclick={() => { count++; dialog.showModal(); }}> :) </button>

<dialog bind:this={dialog}> <p>Hello, I was clicked {count} times</p> <form method="dialog"><button>Close</button></form> </dialog>


Formatted so I can read it (on mobile...):

  <script>
    let count = $state(0);
    let dialog;
  </script>

  <button
    onclick={() => {
      count++;
      dialog.showModal();
    }}
  >
    :)
  </button>

  <dialog bind:this={dialog}>
    <p>
      Hello, I was clicked {count} times
    </p>
    <form method="dialog">
      <button>Close</button>
    </form>
  </dialog>
The inline onclick handler doesn't seem super readable, but the rest is fine.

I assume for most real world, non-trivial cases you'd be defining the click handler inside the scripts above.

This is a very simple component, and its already a mess.

Something with more HTML and javascript is going to be a nightmare.

Then theres the problem of sharing state, the world is far more complicated than something simple in isolation.


If you do this you might as well just write JavaScript without using WebComponents. APIs like innerHTML, querySelector, addEventListener have been around for decades.

Well for one thing, I don't find that to be particularly succinct or nice example. There's a lot going on there:

- HTML in a string. No syntax checks. If you interpolate it, you have to escape manually or you could create trivial XSS vulnerabilities. Your editor will probably not syntax highlight it, making it harder to tell when you break it.

- Manually formatted update logic that is redundant with the string. Not so bad here, but try formulating a practical large component.

- Completely ad-hoc state management, no reconciliation. Again, fine for Hello World, not fine after that.

Using raw WebComponents, your application has to care about all of this on its own and more. You can use templates and slots (which you should) but that makes this even messier IMO and brings back the split that React became famous for getting rid of. We left manual DOM reconciliation, ad-hoc data flow and non-reactive components that manually call a render routine at arbitrary points for a reason: it sucked. It made for buggier, harder to maintain code. It can be done, but usually any decent app will wind up encapsulating patterns into utilities that get reused. And... That's precisely why we want a good library. That's what I want, a library that packages up good reusable patterns for constructing UIs effectively.

And that's exactly my point, which is WebComponents or not, the solution is still basically the same, use libraries. Which begs the question: if I have to use Lit, what is the point of "using the platform"? What is WebComponents doing for me here that React wouldn't be? And in my case, I struggle to see it.

Interoperability? The old way of embedding external components works quite fine. Switching APIs where you pass a DOM node for something to mount into and get back an object with an API to one where you instantiate a component and communicate via properties and events feels like a strictly lateral move. I can't think of a condition where this would be particularly more convenient, but I can think of some where it is actually less. Next.

Isolation? Yes, WebComponents does add tools to provide better isolation between components. For some use cases this is a genuinely useful feature, although it also isn't without tradeoffs (I mean, the flexibility of having things be not isolated comes in clutch sometimes, it's hard to argue this.) But I use React primarily to construct components in an application, where I find this property more undesirable than desirable.

The one thing I thought could be cool with WebComponents is if you could build them in pure HTML when you only needed basic templating, but no: the design they went with always requires subclassing in JS AFAIK.

I could go on and get more specific, but I feel like people will pick everything I just said apart quite enough. I hope I'm at least able to make the case that:

- I do in fact, get the general gist of WebComponents.

- I still don't like it despite that.


> If you interpolate it, you have to escape manually

Well, for some definition of "manually".

If you have untrusted variables to interpolate, you can do

    this.innerHTML = html`Hello ${name}!`
Where html is a function that escapes the variables.

The arguments brought forward against web components all seem to fall under "But it does not contain every functionality you want to use out of the box". Personally, I don't think browsers should provide more and more functionality, but rather a good base to build upon.


Oh great, so where does DOM provide that function?

The great thing with software is you can write your own functions. It would look something like this:

    function html(strings, ...values) {
      let result = strings[0];
      for (let i = 0; i < values.length; i++) {
        result += escapeHtml(values[i]);
        result += strings[i + 1];
      }
      return result;
    }

They should package up a reactive UI framework built on top of the DOM so we can stop having to rewrite it every time we start a new web app.

The reason react sites are bloated is because there are a lot of things that are easy to do once you have better DX, there is nothing stopping you from making completely static sites with no js (solid-js 2.0 has a whole "htmx-like" mode that lets you have good interactivity even with js disabled).

WebComponents are useful for cases where you want something slightly more integrated than an iframe or want to make a library of small widgets


> Isolation? Yes, WebComponents does add tools to provide better isolation between components. For some use cases this is a genuinely useful feature, although it also isn't without tradeoffs

I hate the guts of every site that uses those. Shadow DOM makes the site hard to operate and fix programmatically from user end.

I mean, these days I can just throw an LLM at it so I don't care as much, but sometimes I want to still do things hands-on. Not to mention, Shadow DOM is often a difference between "simple userstyle/userscript that is allowed at work by security extensions" vs "requiring things that are banned on corporate-managed browser".


For starters, that it introduces weird special cases to DOM parsing and structure, thereby breaking .isEqualNode whenever <template> elements are present.

>What do you not like about WebComponents?

I think the problem is not what I don't like about WebComponents individually. The problem is that to use WebComponents, you have to use JavaScript anyway and if it does not do exactly what you wanted, then you're right there, in JS, ready to DIY the problem away.

If WebComponents were purely declarative, I would try harder to use them.


WebComponents are some of the coolest APIs on the web if you ignore all the ways they fall short and make everything else harder

> The webcomponent code seens quite sane to me

If that's a joke, it passed over everybody's head here. I can't imagine it's serious, but it still doesn't sound like a joke to me.


Isn't it missing a removeListener?

If that is what sane web component code looks like, I never want to use it ever.

(preface by saying I use webcomponents regularly, I can show you a game that's currently online where they're used extensively)

I don't really understand why people are complaining that this is unmaintainable on the replies to this snippet but there's plenty of things with webcomponents in general that could be better done on the browser side.

For one the innerHTML (or some new method) should be able to reference a `template` directly. If you want to use templates you need to do:

``` const template = document.getElementById("templates-materialization-base").content;

this.appendChild(template.cloneNode(true)); ```

And if you want to use templates then you need to render the templates in a part of the dom that is parsed before this JS is run - there's no way of specifying dependencies for this kind of things - which is a short-coming of the platform as a whole.

One the same topic there's no way of providing an url for retrieval of a fragment that contains templates and have it parsed directly and available to subsequent JS if you could do:

`<HTML-FRAGMENT href="/my-endpoint/returns-templates.html" required="true" id="MAIN_FRAGMENTS">` or doing `fetch("/my-endpoint/returns-templates.html", {DOCUMENT_ID="MAIN_FRAGMENTS")` would parse the result and make it available to the browser engine then you could have on subsequent JS:

`<script requires="MAIN_FRAGMENTS" wait="GLOBAL_LOADING_INDICATOR`>....</script>` or in a file/module `requires DOCUMENT_ID: MAIN_FRAGMENTS` and the file/script execution would block until that would be available, just that would go a long way. You could specify fragments (the component DOM templates to be used) that are needed for the webpage to work at all, fragments that would only show loading for their own content, etc (you could style the TAG with a pseudo-state indicator, so if it had a wait not resolved yet, CSS could target it with `MY-TAG:state(waiting)`).

Another issue is `attributeChangedCallback` - this doesn't take into account how most of the times one can/will update the attributes of an element, so it fires once for each change, even if you made X changes in one swoop, making it so that you have to have to make a more complex "update_render" function to work around that, or lots of small functions that are composable (ideally, but not practical or wanted in many cases where the whole thing is to be treated as a single update) or create a `batched_attributeChangedCallback` that you implement yourself as part of a class mixin and then extend the HTMLElement class on declaration and use that, because most of the times you want to re-do/update the whole component and when you have multiple properties (equivalent to react props and similar in other frameworks) and multiple components this can result in expensive updates to the page that the user feels as sluggish on their end. Something like:

`batched_attributeChangedCallback(attributes, old_values, new_values, batch_timer_window: 200, batch_timer_fn: true)` where the default for the batch window is 200ms, but can be provided a `fn` that does the evaluation to. So even if the updates are done independently but under the timer window they're batched instead of run 1 by 1 (or immediately if the `fn` returns true, defaulting to true when it's independent 1 by 1 updates).

I usually have a function `_set_handlers(boolean)` that is used on connected and disconnected callback but this is just plain js organisation:

``` _set_handlers(toggle) { let act = toggle ? "addEventListener" : "removeEventListener";

  window[act]("some-window-event", this._maybe_ready);
  this[act]("some-component-event", this._some_fun.bind(this));
} ```

And the connectedCallback just calls `this._set_handlers(true)` and disconnected `this._set_hanlders(false)`.

I use them, you can use them to great effect, but I think the biggest problem is the lack of connection between the different APIs. This includes things like indexDB as well, it's not very complex, but it does look messy on "first-glance". Like your example, but when you actually read it, it's pretty straightforward (ignoring the bad/non-existing functionality/APIs). The same with SSE. This could be a great solution for many things, but you always need to wire up yourself how it works. Would be great if you could declare "receiving beacons", at the page level, or component level, with automatic teardown on removal/navigation. So a component could have `static SSE_BEACONS() { ["https://something.com/path"] }` and would mark the state of the component automatically with `BEACONS: [WAITING | CONNECTED | ERROR]`

The loading indications, still seems strange that after 40 years of web development the transition between webpages are basically "white page", "freeze current view". Just having some way of specifying loading states would make any webpage feel 90% of a webapp between page transitions. Make it be a restricted subset of CSS that can be included as a header at the document top level and cached for subsequent visits `<LOADING_STYLES version=X><MAIN>background-color: white;</MAIN><LOADING_INDICATOR>shape: square; rotation: 1s; main-color: blue; secondary-color: turquoise</LOADING_INDICATOR></LOADING_STYLE>`. This would also allow to evolve it while maintaining strict backwards compatibility by starting from a very basic set of possible values - but just this would make website loading a much more app like experience. It could be used together with webcomponents too: `<MY-WEBCOMPONENT ...><LOADING_STYLES>...</LOADING_STYLES></MY-WEBCOMPONENT>` that then could be automatically derived from the `states` I mentioned prior.

Anyway... Spent too much time with frameworks and webcomponents by now. It probably would also make writing code for LLMs less error prone.

Just a rant, don't take it too seriously, but sometimes I wonder with all the resources poured into frameworks by the big Gs, that must amount to millions and millions of dollars when you take into account the salaries some of the people working on them take, if it wouldn't have been better spent somewhere else on the "base" implementation.


> ``` const template = document.getElementById("templates-materialization-base").content;

this.appendChild(template.cloneNode(true)); ```

It was pointed out to me that `this.appendChild(document.importNode(template))` is faster due to "adoption" (`importNode` clones and adopts the nodes for the current document at the same time/single tree walk, saving appendChild from having to do a separate adoption pass/tree walk; template.content exists in a fake separate document so the nodes need to be "adopted" by the current document).

Which is a point in favor of the current complexity of the JS API for working with templates. I agree that "HTML Modules" would be nice to have and more declarative template usage that doesn't require the Shadow DOM would be lovely. Both are still proposed standards going through various debate cycles.

> making it so that you have to have to make a more complex "update_render" function to work around that

One potential simplifying answer when trying to deal with batched updates to attributes is a simple "dirty flag" and requestAnimationFrame callback as your "update_render". Most things changing more than one attribute at a time are likely going to do it all between animation frames and even if they can't, rAF provides other throttling that prioritizes user interaction.

There's still complexity in that approach, but it is isn't as complex as trying to build a throttling timer.

> I usually have a function `_set_handlers(boolean)` that is used on connected and disconnected callback but this is just plain js organisation:

It looks like you could replace some or most of your `_set_handlers` function by implementing `handleEvent(e)` for the Web Component. It's an auto-registered/unregistered handler at the component level of the event bubble.


Thanks for the `handleEvent` tip, that's useful for component wide events, but it creates some problems if you need to prevent the event at the input level, because some (specially those related to submit) will need to pass through the form to bubble to the component level and will trigger it, and preventing it at the component level wouldn't cut it if I'm not mistaken? It also doesn't cover events on the document, but that can be worked around and perhaps even clearer to have it separated (`set_global_events(boolean)`). it's a good option to be aware of but I also like segregating them in that way since it allows me to turn on/off from other parts of the component outside the lifecycle calls (or even from a parent component) - sometimes useful too.

You would also need to switch inside the handle event to deal with the different types plus the different targets that might be bound to those events inside the component so I think that for anything with a modicum of complexity it doesn't cut it, unless I'm misunderstanding how to apply them in those cases (pattern matching with multiple function heads would definitively make it read like a declarative model but is not really serviceable in JS even if you syntactic sugar it, you wouldn't be able to describe `target` html elements probably - like:

`handleEvent({type: "click", target: {TAG: "input", id: "some-id", type: "input"}) { ... }`

and then another

`handleEvent({type: "click", target: {TAG: "button", ...}) { ... }`

, etc).

The importNode I think there was some issues with some properties not being copied outside of clone? I can't recall why I defaulted to `cloneNode`, could also be I just missed the `importNode` altogether or misread it at the time, but it has worked fine since then so I didn't look for ways to do that better.

Regarding batching, I basically have been using the same class extension since I've started working with webcomponents and it works, it uses a timestamp to control if we're inside the batch window for the inner working and then the "apply/build" part of each component has to compare those and decide - this is the part I would rather not have to code and have it handled natively (some events might actually run a single pointed update while others run the "full" apply/build function). It's nonetheless a bit more wiring and someone that never seen it might have the same reaction as some people here, even though they're fine in importing 50 libs, or 20 lines of useMemo with useState and useWhatever but heavens forbid they might need to read a 50loc class module. At some point many components need to ditch the `attribute` change handles as parsing the attributes into json/objects can get expensive, and you need to start doing parent->child direct data transmission and this is the only real bottleneck I've experienced when using attributes and requires an overhaul to how data is passed into the children (either from parent orchestration from top level attribute parsing that is fairly less taxing when done once only, but at a certain point can still stall the main loop just the same, at which point you need to parent-level requests, websockets, SSR that then you can pass directly to the child child_component._set_whatever_data_function(data) or setting the prop directly if it's workable in the particular case).

The thing with being able to load html fragments (or, even if whole html docs would be what we settle into) and having it be available would be that you could separate them into cacheable requests - so if you have `/essential-templates.html` , `/signed-in-templates.html`, `/special-page-templates.html` they could be cached, which means they would only need to be refetched if they changed or TTL expired. But with no way of declaring dependencies it can (will) also introduce bugs - if there were we could probably do some cool (as in useful) things too. I guess this is what many frameworks end up doing when they need code splitting, modules, and to create the initialization tree of a page/component, but then need to be re-implemented from scratch by each and leave vanilla webc out of an inbuilt system for it.

Here I might just be lacking some knowledge, and perhaps with TS it would be possible to, but the same way you write `static get observedAttributes() { return ["data-flux"]; }` would be great to have a `prop` like system, say `observedProps` that specified the `required` props when the node is to be attached to the DOM (not before, since `createElement` doesn't take options right? for the constructor or otherwise) for the cases you're adding elements from inside another component, that don't derive their inner state/data from attributes due to parsing cost and require directly setting those props.

Shadow dom is not something I use very much because I'm not creating widgets for third parties, or having to have locked components, but I do understand their existence and think it makes sense to have that mode too.

I imagine this is a complex topic altogether to push forward in the context of wrangling so many disparate interests (companies with lots of power and the only real players when it comes to browsers) but nonetheless not having them forces everyone and the webplatform to be way more spaghetti and troublesome than it needs to be. And I think that at a certain level, these issues go beyond the implementation and RFCs into the way browsers actually work, their engines and choices made before, that don't seem to be reasonable in hindsight when looking at from where we landed but asking for a new model will have 0 traction (of the type that matters) since there's too much at stake for what I imagine are "other" reasons.


Why are your component styles global not encapsulated within the component?



Consider applying for YC's Winter 2027 batch! Applications are open till November 2.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: