I was exploring current front-end frameworks and libraries to get a better understanding where we stand and how we got here. I was especially interested in the current capabilities of native web components (in vanilla JS) using web standards and how they compare to these frameworks and libraries. I decided to write up all my notes into this post, maybe they prove useful for somebody. Let me know what you think!
First, some terminology.
Imperative versus declarativeThese are general programming paradigms but in the context of front-end development, a declarative representation of UI has become a standard approach for front-end web development. This offers a way of writing templates/views with data binding and separating the control flow. It's also described as creating UI as a function of state. Imperative on the other hand is the sequential way of taking steps to complete a task. We all know imperative using basic vanilla JS or jQuery for DOM manipulation.
When you are talking about views, you are talking about templates and their engines. And there are several flavours.
String based template engines
These are template engines like mustache which uses innerHTML
and are the least efficient but they used to be popular because DOM manipulation was slow in older browser. And innerHTML also has security concerns. But the concept is DOM independent, which could be seen as a pro, especially if we think about server side rendering (SSR). By the way: Twig is also a a string template engine, but in PHP.
DOM based template engines
Examples which use these are Angular, Alpine and Vue (Vue only when using DOM templates). In all cases, the templates are parsed which can cause issues. For instance, (Vue developers warn about DOM templates) because they can be unpredictable: the browse renders the markup into a DOM template which is then used by the framework and it might disagree on some things. It's also impossible to do SSR since we have no browser server-side.
JSX
JSX is not a template or engine. It's declarative syntax that’s used to express the virtual DOM after it is parsed by babel into Javascript objects. While JSX may look like HTML templates, it's not. It's not even React specific (Stencil uses it too) and is being used in to create components which share markup and logic. This is a better fit for component based architecture which React started.
Virtual DOM
Not a template but a technique to effectively update the DOM. The best explanation I found on the virtual DOM is actually a Svelte blog post which explains how React uses this to diff the DOM against the VDOM and make the necessary changes.
All three major frameworks (Angular, React, Vue) have converged to the component model and a MVVM (Model-View-ViewModel) architecure.
Model (M)
The object which contains data and business logic.
ViewModel (VM)
Component code data binding into the view (and potentially back) and managing simple state (presentation logic).
View (V)
How the visuals look in the browser. The DOM + styles.
Note: depending on the implementation, you might only have a VVM architecture, for example if you have a simple React site which pulls data directly into the component, without using a data layer like GraphQL.
Components, Custom Elements and Web ComponentsThe three major frameworks use Components but none of them use web components by default. What do we mean by native web components (or just web components)?
Native web components use three main technologies. Although I've seen ES module specification being mentioned as a fourth.
- Custom Element
- Shadow DOM
- HTML templates and slots
You can just create Custom Elements without shadow DOM and template/slots, it's optional. In fact, many developers just use Custom Elements because the Shadow DOM has challenges of its own (specifically with styling and SSR, more on that below).
So why don't the major frameworks use them by default? Partly because the technology wasn't mature enough until recently (and still developing) but also because they solve different problems: frameworks aim to solve the ViewModel part in a declarative way, and when we look at a custom web component built in vanilla JS you will see it lacks any form of data binding.
Still, you could add basic one-way data binding (based on this post) with some code.
lit-html + lit-element
The smart people behind Polymer have created two libraries: lit-html
which make creating declarative templates - UI as function of data - easier (like the frameworks do) but with minimal overhead. And lit-element
which essentially is a base class making it easier to create Web Components.
Lit-html feels like JSX but internally it leverages tagged template literals to very effeciently update the DOM. Lit element creates Custom Elements with shadow DOM by default. So it seems like a nice way to create web standard components with data binding, minimal overhead and a lot of flexibility
In this repo are two simple examples of a lit-html and lit-element.
Stencil
Another tool is Stencil from the smart people of Ionic. It's not really a framework nor a library but a compiler which creates web standard web components or entire PWA's. It uses JSX and Typescript and compiles to native javascript. This makes it perfect for integration with other frameworks or tools like Storybook, although you can also use Stencil as a component library on its own.
Here is my Stencil playground repo.
Compared to lit-html/element, Stencil aims to be more: a toolchain for building a reusable components with a standard structure using JSX and Typescript. And, being a compiler, Stencils aims to be future-friendly by constantly optimizing the compiled code. Stencil also makes the choice for shadow DOM explicit: you have to set this with an option if you want, the default being disabled.
Quoting from their own site: "Stencil lets the core developers put their time into building components, while the robots are focused on optimizing everything else, and adjusting to new standards".
Another advantage of Stencil is you don't have to use CSS in JS but can include separate CSS files for your elements.
Svelte
Svelte isn't really a tool to create Web Components but rather an alternative with a different approach compared to the existing frameworks. I still think it's worth mentioning because it does have some similarities with Stencil as they are both compilers and compile into native javascript components which can be used by third parties (or standalone). But Svelte explicitly does not use a virtual DOM, it instead compiles to imperative code that updates the DOM, which may be faster.
By default, Svelte does not create Custom Elements but it is possible with an option. Svelte has TypeScript and better support for testing on the roadmap. Being a compiler it aims at designing the best component authoring experience with as less code as possible.
Demo repo with a Svelte component
Note: there are more libraries and tools besides Lit/Stencil/Svelte to create Web Components which I didn't consider in this post.
What problems are we solving anyway?So why might we prefer native web components over components provided by the frameworks? Interoperatibility and reusability using standards. Stencil clearly aims for being the tool do to create web components which can be used in various frameworks without any dependencies. You can do the same with lit-element, if you don't mind the small dependency. And you can already integrate Custom Elements without problems in most frameworks, although you might ask yourself if you need it for smaller projects?
Smaller projects which don't need all the features of a framework can use web components (+ maybe some vanilla JS) and end up with a much smaller codebase, faster app and use web standards making it more future proof. And when you use tools/libraries like Lit or Stencil you still get the benefits of having at least some standards in your codebase.
But wait, don't frameworks like Vue and Angular also offer the option to convert their "framework components" directly into web components? Yes, for instance, you can wrap a Vue component into a Vue web component using a library. However the resulting web component still has a dependency on Vue, which makes it less ideal to share your components. Having said that, it all depends on your needs: if your organisations only builds Vue apps, you could create a Storybook with Vue components share them across your applications and be very happy.
Component styling using the Shadow DOM
The shadow DOM creates a separate DOM tree of your element next to the normal DOM (also sometimes called the light DOM) and has the potential to create an elegant solution for scoped styling of components. Again, frameworks have solved this problem with custom solutions. Angular for example has 3 view encapsulation modes from which you can choose, and Shadow DOM is even one of them now. Another approach you might encounter is using CSS modules.
But using the web standards we can style web components using shadow DOM encapsulation. We can use custom properties to create a "styling API": see the header color in this example.
As you can imagine that approach has its limitiations (when exposing large amounts of properties) which is why ::parts
and ::theme
have been proposed and look like much better solutions (explainer and demos here) but have less browser support than custom properties.
Traditionally, websites with limited reactivity used to handle styling by toggling class names. But in a component world we are dealing with states and properties which are not necessarily tied with styles. Also, class names don't offer fine-grained control which direct style manipulation does. Libraries like Styled Components understand this and are popular in application built with React which keep state out of the DOM. For the shadow DOM, constructible stylesheets is a standard (draft) which seems to promise a better way to compose components styling. Together with loading CSS modules would even mean we can keep CSS in separate .css files instead of CSS in JS which is not loved by everyone.
Meanwhile, Stencil offers a way to include component stylesheet files.
Other issuesSSR and Shadow DOM
You can't declaratively create web components with shadow DOM (issue) without javascript: it doesn't exist until we customElements.define()
the element and attach the the DOM to the root. This means it's difficult to server side render web components. Why would you want to do that? The idea is that components should still be usable without javascript and progressively enhanced with javascript.
Pollution of the global registry namespace
Currently all web components are created in the http://www.w3.org/1999/xhtml
namespace which can cause collisions. There are issues and proposals to fix this.
Accessibility
Web components should be built with accessibility in mind, just like any other HTML. Putting the dependency on javascript aside, accessibility is currently a second class citizen in web components and you have to be very aware that when you create web components you start from scratch and have to implement it yourself. The Accessibility Object Model is an API which will make accessibility a first class citizen of the extensible web and should make this easier/better. But it certainly deserves more attention.
After having spent (probably too much time) exploring Web Components, various frameworks, tools and libraries and upcoming proposals, I feel web components have come a long way since I first played with them in Polymer 1.0 back in 2015. Browser support is good and while there are still some issues, I'm excited to try and build a small project using them.