Comment by mg
17 hours ago
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...
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 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.
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...):
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.
WebComponents are some of the coolest APIs on the web if you ignore all the ways they fall short and make everything else harder
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
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.
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
Oh great, so where does DOM provide that function?
2 replies →
> 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.
> 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.
If that is what sane web component code looks like, I never want to use it ever.
>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.
Why are your component styles global not encapsulated within the component?
(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";
} ```
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.
[dead]