And, how it compares to React as well as Vue.
This a cross-post of a Sep 24, 2018 article from Medium that takes advantage of my recent decision to use Grammarly in my writing (so, small edits have been made here and there), thanks for looking again if you saw it there đđ˝ââď¸ and welcome if this is your first time.
Edit: (1/20/19) Update code samples and repo to apply
lit-element@2.0.0
and associated updates to the API available there. These changes include but are not limited to a pre-establishedthis
context of template-based event listeners, support foradoptedStyleSheets
, updated property metadata, and default type serialization.
In the standard week of a software engineer, youâd be hard-pressed to avoid a good âthis approach versus thatâ article or two. In the world of frontend, often this takes the shape of how framework or library X compares to the same in Y. This week mine took the shape of A comparison between Angular and React and their core languages. In other weeks it might be three or seven different articles! However, more articles a week does very little towards making sure you find really solid writing, logic, or learning in any one of these articles. I think we feed the self-fulfilling prophecy that the more something is written about the more others will also write about it. The cycle is even faster to the point of being almost unwanted when you focus specifically on what can be perceived as âmajorâ players the likes of Angular, React, or Vue.
Sadly, almost as a rule, the more something is written about, the harder it is to find quality writings on the subject. Thatâs why itâs quite refreshing when you do find a quality comparison of technical applications in written form, and I did just that several weeks back when I was delivered Sunil Sandhuâs I created the exact same app in React and Vue. Here are the differences. Not only does the writing avoid explicit favoritism, despite Sunil making it clear that heâd worked predominantly with Vue up till the point of his writing, it went the extra step of not comparing the two allegorically but with real code; code with just enough complexity to get to the important points, and just enough simplicity to be parsable by the reader without investing inordinate amounts of time to the process. Whatâs more, as an engineer thatâs only worked around the edges of React applications or on demo code, while having written not a line of Vue, I really felt I had gained a deeper understanding of each upon completing the article.
Itâs definitely this sort of quality writing on a subject that inspires others to get into the game; even if itâs just me, it happened and youâre a part of it now, too! Sometimes this is a direct response in the vein of âIâve got opinions I want to share in this area, tooâ, but for me over the last few weeks I just could stop thinking, âhereâs the beautiful piece talking about React and Vue, where is the article doing to same for technologies I rely on?â As a long time creator of web components, and more recently a heavily invested user of LitElement, currently under furious development by the Polymer Project team at Google, I am keenly aware that there has yet to be built a beautiful library to house the literature on the subject. As it stands today, you might not even need a whole newsstand to store the written work on the subject. Hereâs a short list of places you might choose to start:
- Let's Build Web Components! Part 1: The Standards by Benny Powers, the first of a series introducing the technologies for dev.to
- The future of Polymer & lit-html by James Garbutt, a deep dive into how the various products that come from the Polymer Project compare to each other
- The Web, its components (their reusability), its frameworks, and its discontents. and Generating some next-generation web components by yours truly, a general introduction to the space and a rundown on how teams I work with get started on new components, respectively. -And a big list of Awesome lit-html maintained by Serhii Kulykov
However, much of this is focused on internal comparison. So, starting from the great work the Sunil had already shared with the world, here is my attempt to take his level headed comparison of these libraries at an application level one step further and include an analysis of the same app built with LitElement.
To that end, letâs get started!
There are certainly some differences in how the files are structured in this application. The Polymer CLI doesnât support the src
/public
distinction that was on display in both the React and Vue applications, at least not right out of the box, so I chose not to fight it much. In support of that decision, you will see an index.html
file in the top level of our application; this replaces src/main.js
that you found in the Vue application and src/index.js
in the React application as the entry point to the application. Iâve slimmed it down for the context of this being a demo, but even in the majority of delivery contexts there isnât much more you need beyond:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>Lit-Element To Do</title>
<link rel="stylesheet" href="src/index.css" />
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<to-do></to-do>
<script type="module" src="./src/ToDo.js"></script>
</body>
</html>
There are still a few browsing contexts that require polyfills, and I like to rely on the type="module"
to nomodule
trick to support delivery of the smallest amount of transpilation in modern browsers, but beyond that thereâs not much else you could want in an entry point to your to-do application.
Before we dive too deep, letâs take a look at what a LitElement based web component might look like:
Web components can easily take on the single file component approach that you see with Vue, however here Iâve split out the styles into separate files. Uniquely, youâll notice that the styles are imported from a JS file rather than a CSS file, this is to keep the import system applied herein more closely in line with what is possible in the browser and to take advantage of the capabilities provided by lit-html
the rendering engine that underlies this base class offering.
Above you have the styles as applied to a css
template tag that supports the implementation of these style via Constructable Stylesheet Objects which allow your custom elements to share the same <style/>
tag across multiple instances of itself. Applying your styles in this way will allow for greater performance as this feature becomes available in browsers and is shimmed internal to LitElement
for browsers that have yet to implement this API. If you love the Vue approach of single file components, nothing is keeping you from placing this in the same file as your functionality and template. However, having the code split out like this makes the promotion of the styles included to shared styles (those used in multiple components across your code base) very easy.
How do we describe and mutate data?
static get properties() {
return {
list: {type: Array},
todo: {type: String},
};
}
constructor() {
super();
this.list = [
this.todoItem('clean the house'),
this.todoItem('buy milk')
];
this.todo = '';
}
todoItem(todo) {
return {todo}
}
How did LitElement do that?
First things first, LitElement is extending HTMLElement, which means weâre making Custom Elements every time we use it. One of the first superpowers that custom elements give you is access to static get observedAttribute()
which allows you to outline attributes on your element to observe. When these attributes change, attributeChangedCallback(name, oldValue, newValue)
will be called which allows your element to respond to those changes. When using LitElement the properties listen in static get properties()
automatically be added to static get observedAttribute()
with the value of that attribute being applied to the property of the same name by default. If you want (or need) to extended functionality here, you can further customize how each property relates to the elementâs attributes and relates to the rendering of the element. By adding an attribute
key to the definition object, you can set the value to false
when you donât want the property in question to be settable via an attribute, or provide a string to outline a separately named attribute to observe for this propertyâs value. The converter
property is used above to outline a specific how to serialize the value set to the observed attribute, it will default to the appropriate processing when the type
property is set to Array
, Boolean
, Object
, Number
, String
, however you can customize this with a single method for bi-directional serialization or an object with fromAttribute
and toAttribute
keys to outline the serialization that should occur for both consuming and publishing that attribute. reflect
will track as a boolean whether the value of the property should be published directly to the attribute on all changes, and hasChanged
allows you to prepare a custom method for testing whether changes to the propertyâs value should trigger an update to the elementâs DOM. When a hasChanged
method is not provided, this test is made by strict JS identity comparison meaning that the data managed as properties by LitElement plays well with immutable data libraries. This extended property definition might look like:
static get properties() {
return {
roundedNumber: {
attribute: 'number',
converter: {
fromAttribute: (value) => Math.round(parseFloat(value)),
toAttribute: (value) => value + '-attr'
},
reflect: true,
},
};
}
Feel free to see that go by in real life via this Glitch. When defined as such, the value of this.roundedNumber
would follow a lifecycle much like the pseudo code below:
<my-el // the `number` attribute of
number="5.32-attr" // <my-el/> is set so we
></my-el> // take the value, 5.32-attr
// run fromAttribute method
parseFloat('5.32-attr'); // parseFloat it, 5.32
Math.round(5.32); // round it, 5
this.roundedNumber = 5; // store it in `this.roundedNumber`
// CHANGE RECOGNIZED because 5 !== undefined;
// run toAttribute method
5 + '-attr'; // append '-attr', '5-attr'
this.setAttribute(
'number',
'5-attr'
); // set it to the attibute
However, this isnât something weâll need to take advantage of for a to-do app, so we should dive into that further as part of a future post.
What all this does under the covers is create a getter
and a setter
for each property to manage its value and to call the appropriate lifecycle methods when the values change as outlined in your hasChanged
method. This means you can manipulate the state directly (i.e. this.name = âJohnâ;
) much like you would with Vue, however youâd fail to trigger an update to the template when not altering the identity of the data (this.list.push({todo:'Does not Mutate Dataâ}
) doesnât change the identity of the array, which means a new render isnât triggered). However, additional flexibility in your dirty checking is supported as desired (i.e. hasChanged: (newValue, oldValue) => newValue > oldValue
would trigger a change only when your value is increasing, so this.demoValue = this.demoValue + 1
would trigger a change, but this.demoValue = this.demoValueâââ1
wouldnât, if you saw a benefit in it). You also have the option to write your own custom getters
and setters
, but again...future post.
Youâll also see my addition of the todoItem
method to abstracts the creation of a to-do item. This is in no way LitElement specific, but I felt it added both simplification and unification to the to-do code as it is used in initialization as well as in creating new to do items.
How do we create new To Do Items?
createNewToDoItem() {
this.list = [
...this.list,
this.todoItem(this.todo)
];
this.todo = '';
}
How did LitElement do that?
If the first thing you said was âthat looks like a mix of both the React and Vue code to create a new to do itemâ, then youâd be right. The direct property access provided by Vue is alive and well with this.todo = '';
and the need for unique array/object identities of React is there too with the use of ...this.list
, leveraging the spread operator to create an array with a unique identity while still including all of the data from the previous array. In this way, the pushing of data into the DOM and receiving it from an event is very similar to what was going on in the React application with only a few differences.
<input
type="text"
.value=${this.todo}
@input=${this.handleInput}
/>
Youâll notice the .value=${this.todo}
syntax. Here you see the template set the property value
to the value of this.todo. This is because value
is one of the few attributes that doesnât directly sync to the property of the same name in an <input/>
element. While you can get the first value of this.todo
to sync appropriately by setting the attribute only, future change (particularly those clearing the <input/>
after creating a new to do) would not update the UI as expected. Using the property value
(and thus the .value=${...}
syntax) rather than the attribute solves that.
After that, youâll see @input
syntax which is very close to the event handling we saw in Vue. Here it is simply template sugaring for addEventListener('input',...
, which is used here to trigger the pseudo-2-way binding that manages the value of this.todo
. When an input
event occurs on the <input/>
element, the handleInput
method is triggered as follows, setting the value of this.todo
to the value of the <input/>
. (Note: Here the input
event is used as opposed to the change
event. This is because change
will only trigger after the blur
event, which would prevent the Enter
button from having data to trigger self-fulfillment of the âformâ.)
handleInput(e) {
this.todo = e.target.value;
}
How do we delete from the list?
deleteItem(indexToDelete) {
this.list = this.list.filter(
(toDo, index) => index !== indexToDelete
);
}
How did LitElement do that?
Array.prototype.filter()
is great for working with data in this context because by default it creates an array with a new identity. Here we directly set the value of this.list
to the filtered array created by removing the item at index === indexToDelete
and a new update to the DOM is requested in response to the change displaying the change.
To make this possible, weâll first bind the deleteItem
method to both this
and the key
(index) for the item in the array and pass it as a property into the <to-do-item/>
element that displays individual to-dos.
<to-do-item
item=${item.todo}
.deleteItem=${this.deleteItem.bind(this, key)}
></to-do-item>
This initial pass at the LitElement version was refactored directly from the React application, rather than a generated application, and as such shows how most of the techniques therein were possible in a LitElement context. However, there are some realities that this sort of approach to parent/child interactions that we should go over. So as not to disrupt the conversation around the two approaches relativity, Iâve grouped this with similar ideas in the Or do we have it? section below.
How do we pass event listeners?
<button
class="ToDo-Add"
@click=${this.createNewToDoItem}
>+</button>
Here again, we see the Vue shorthand syntax pushing our events into React like handlers. However, as before, thereâs only the slightest of magic (just straight sugar) in the template as it applies addEventListener
to the element in question. Youâll also notice that the keypress
event needs to be handled in its entirety as well.
<input
type="text"
@keypress=${this.handleKeyPress}
/>
The event is processed directly for e.key === 'Enter'
just like you would with VanillaJS.
handleKeyPress(e) {
if (e.target.value !== '') {
if (e.key === 'Enter') {
this.createNewToDoItem();
}
}
}
How do we pass data through to a child component?
<to-do-item
item=${item.todo}
.deleteItem=${this.deleteItem.bind(this, key)}
></to-do-item>
For each of our todos we need to pass down the value of item
and deleteItem
to accurately inflate the UI and trigger functionality on interaction. In both contexts, weâve simplified the properties by pairing them directly to attributes so you would think that we could apply both directly as an attribute. This idea works great for item
which is serialized as a String
and as such easily transforms from an attribute to a property, but for the deleteItem
method, passing a function this way is no good. That is why youâll see the .deleteItem
syntax signifying that we are setting this value as a property onto the <to-do-item/>
element instead of as an attribute. Weâll discuss a caveat of this approach in the Or do we have it? section below.
How do we emit data back to a parent component?
<button class="ToDoItem-Delete"
@click=${this.deleteItem}>-
</button>
In that weâve passed a bound method into the value of deleteItem
when we hear the click
event on our delete button we can call that method straight away and see its side effects in the parent element. As I mentioned in How do we delete from the list? this concept is something weâll revisit in the Or do we have it? section below.
And there we have it! đ
In short order, weâve reviewed some central concepts around using LitElement, including how we add, remove and change data, pass data in the form of properties and attributes from parent to child, and send data from the child to the parent in the form of event listeners. Hopefully, with the help of I created the exact same app in React and Vue. Here are the differences. this has been able to give you a solid introduction into how LitElement might compare to React or Vue when taking on the same application. However, as Sunil said best,
There are, of course, lots of other little differences and quirks between [these libraries], but hopefully the contents of this article has helped to serve as a bit of a foundation for understanding how [each] frameworks handle stuff
So, hopefully, this is but a beginning to your exploration, no matter which part of the ever-growing JavaScript ecosystem that exploration may take you.
Github link to the LitElement app:
https://github.com/westbrook/lit-element-todo
Github links to both of Sunilâs original apps:
https://github.comsunil-sandhu/vue-todo
https://github.comsunil-sandhu/react-todo
Or do we have it? (reviewing the effect of some differences)
If you have been enjoying the code only comparison of LitElement to React and Vue, please stop here. Beyond here be dragons, as it were. Having built a LitElement to do app in the visage of a React to do app, I wanted to look at what these features would look like relying on more common web component practices, and I wanted to share those in the context of this close comparison.
Reusability contexts
Part of the concept behind the componentization of the web is reusability. We want to be creating components that we can use in this app over and over again, while also having the possibility to use them in other apps both within our organizations and beyond. When thinking about this act as part of a Vue or React application where the only context for use of the components that you are creating is inside of a Vue or React application, it is easy to get caught in the ease and fun of things like passing a method to a child.
<to-do-item
.deleteItem=${this.deleteItem.bind(this, key)}
></to-do-item>
The parent will always be inside of an application and the child will always be inside of an application, so the technique just makes sense and has become commonplace. So commonplace, that is is often the first question I hear when engineers with experience in React start thinking about working in web components, âHow do I pass methods to children?â Well, the answer is above. However, when you choose to do this, you are choosing to take away one of the superpowers of using the platform, and thatâs the ability to work outside of an application. Have you ever had issues working with an <input/>
element outside of an application? Ok, dumb question. Have those issues ever been something that a little visit to MDN couldnât fix? However, this LitElement based <to-do-item/>
element, and the equivalent <ToDoItem />
in the React app both expect to be delivered a method to call as deleteItem
this means there would be no way to apply them with pure HTML that wouldnât find them erroring out when clicked. <to-do-item></to-do-item>
should be given the ability to be used in this to do app, in another to do app, or in anything really, and one of those options is directly in the HTML. To make this possible, we want to take a page out of the Vue to do app, and loosely couple our items without lists.
Loose coupling
Beyond the contexts of reuse, that passing a method into a child prevents, a child requiring a method be provided essentially creates an upward dependency chain that out current tools canât ensure. import {foo} from './bar.js';
can ensure that the child dependency graph is static, but we have no concept of requiring functionality on a parent. This means that the implementer of our <to-do-item/>
component has to grok this reality and manage the parents that it is deployed in as such. A tight coupling. The Vue to do app, avoids this for the most part by instead of calling a provided method it $emit
s an event when the delete button is clicked:
<div class=âToDoItem-Deleteâ @click=âdeleteItem(todo)â>-</div>
// ...
deleteItem(todo) {
this.$emit('delete', todo)
}
This, of course, requires a little more code, but the flexibility that it gives us is amazing. Here is the same code as applied to the LitElement based <to-do-item/>
:
<button
class="ToDoItem-Delete"
@click=${this.deleteItem}
>-</button>
// ...
deleteItem() {
const event = new CustomEvent('delete', {
bubbles: true,
composed: true,
detail: {todo: this.todo}
});
this.dispatchEvent(event);
}
A further benefit of this approach includes the ability for something other than the immediate parent being able to be listening to the event, something I canât find adequate documentation on immediately for Vueâs $emit
method. (This documentation seems to imply that it creates a non-bubbling event, but it isnât exactly clear on the subject.) When bubbles === true
the event will bubble up your application until e.stopPropagation()
is called meaning that it can also be heard outside of your application. This is powerful for triggering far-reaching side effects as well as multiple side effects and having a direct debugging path to actions at various levels in your application or outside of it. Take a look at how that looks in the full application in the event
branch.
Delivery size
react-scripts
is shipped as a direct dependency of the React to do app in Sunilâs article, and one of the side benefits of that is that a yarn build
command points into those scripts and prepares your code for production. The same concept is powered by vue-cli-service
in the Vue version of the app. This is great being none of the things that make a developerâs life easier should get in the way of our usersâ ease of use, and that includes not shipping development environment code to production. Whatâs even better is that using the command takes the React app from 388KB (down the wire)/1.5MB (parsed) development app down to just 58KB/187KB, which is a nice win for your users. Whatâs more, Iâm sure the build process is still fairly naive as it comes to build processes and there would be room to shave off further size before actually delivering to production. Along those lines, I hacked preact-compat
into the react-scripts
based webpack config to see what it could do, and it moved the application to 230KB (over the wire)/875.5KB (parsed) for the development app with the production app clocking in at 19.6KB/56KB, a solid jump towards ideal. I look forward to my having brought it up here inspiring someone to create this app from scratch in Preact where I expect to see even better results! In the Vue app, you see a 1.7MB (over the wire and parsed) development app (there seems to be no GZIP on the Vue development server) taken down to an even smaller 44.5KB (over the wire)/142.8KB (parsed). While these are both great results, approaching the same concept through the use of polymer build
(powered by the settings youâll find in the polymer.json
config for the app) takes a development application of 35.7KB (down the wire)/77.5KB (parsed) and turns it into a production-ready 14.1KB/59KB. This means the entire parsed size of the LitElement application is roughly the same as the over the wire size of the React app, and the over the wire size is only 1/3 that of the Vue app, huge wins on both points for your users. Tying these findings to the ideas outlined by Alex Russell in The âDeveloper Experienceâ Bait-and-Switch is a whole other post, but is highly important to keep in mind when going from a technical understanding of a library or framework to applying that library or framework in your code. These are the sorts of performance improvements that we wonât see on our $3000 MacBook Pros, but when testing with applied connection and CPU slowdowns on mobile with Lighthouse you start to get an understanding of what this might mean for a fully formed application. Much like high school chemistry, with these đŻ point grades, there is lots of nuance...
React To-Do App
Preact To-Do App
Vue To-Do App
LitElement To-Do App
Yes, youâre seeing that right, the LitElement
to-do app gets to CPU Idle almost twice as fast as either the React or Vue applications with similar results across almost all of the metrics deemed important for this audit. Preact comes in at a virtual tie when deployed as a drop-in replacement for React, which most likely means it would run even smaller as the default build dependency. Itâll be interesting if that also cleans up some of the longer âFirst *â times seen in the audit. This means there is certainly more research to be done in load performance and points to a less clear decision on what is the best choice for managing the UI of your application. Iâll save thoughts on a future where Preact must continue to maintain its own component model and virtual DOM engine while lit-html
has the possibility of stripping itself down even further via the pending Template Instantiation proposal for a future post.
Top comments (0)