Reactivity? You Don't Need a Base Class for That
If you have ever looked up “how to write web components”, you may have seen several approaches suggesting to use a base class.
I agree! To me, this was immediately apparent because I din’t want to write a lot of the boilerplate every time I need a custom element.
But I sometimes see “reactivity” getting touted as a major feature of base classes and thought I’d write this quick one to show, well, you don’t need a base class for just that.
What is reactivity
The essence of reactivity is this: when a state changed, the view (or another state) is immediately updated. They are synchronized.
JS frameworks have different approaches for this, with some better than others depending on certain situations. (It’s a fun topic to nerd about.)
For components, this is commonly expressed as: if my property changed, the UI should be updated.
Let’s not dig into details of implementing a reactivity system, but go straight into some vanilla implementation
Vanilla Implementation of Reactivity
Here we want to use standard APIs available for custom elements: HTMLElement.dataset
and the attributeChangedCallback()
hook.
Let’s consider the following code example (also in CodePen →):
class Counter extends HTMLElement {
static observedAttributes = ["data-count"];
connectedCallback() {
this.dataset.count = this.dataset.count ?? 0;
this.onclick = () => ++this.dataset.count;
}
attributeChangedCallback(prop, oldValue, newValue) {
if (prop === "data-count" && oldValue !== newValue) {
this.textContent = newValue;
}
}
}
customElements.define("my-counter", Counter);
In this example we use the standard dataset
that collects all attributes prefixed with data-
. The dataset
property exposes them for reading and writing and, because they have attribute counterparts, they will trigger the attributeChangedCallback()
when modified.
Hence, every time the user clicks the element, the program modifies the observed data-count
attribute and the update to the UI via textContent
is triggered.
The standard for data-
prefixed attributes exists to make sure they don’t override standard HTML attributes. And collecting all values behind the dataset
property means no standard HTML properties can also be overriden.
Nice! But what’s the catch?
The Catch in using HTMLElement.dataset
Every time we modify a property value using dataset
, the method toString()
is actually called under the hood for the value assigned. This is because HTML attributes can only hold strings.
Our example works here because JavaScript (being loosely typed) allows arithmetic operations on stringified numbers. :)
You will experience hiccups, for example, when you need a boolean property, because the stringified “false” is also “truthy”.
The Reactivity Problem
This is the problem reactivity systems try to solve: how do you synchronize state & UI and still restore correct types in your properties after the full reactivity loop?
In my minimal base class WebComponent.io, my approach is simple… we use the pattern encouraged by HTMLElement.dataset
, but use a standard ES6 Proxy props
.
The props
property of WebComponent
works like HTMLElement.dataset
, but a camelCase counterpart using it will give read/write access to any attribute, with or without the data- prefix.
And because it is a Proxy, we are able to intercept this read/write accesses and allow for behavior modification like restoring types.
Therefore, we get some advantage over the standard HTMLElement.dataset
: that WebComponent.props
can now hold primitive types ‘number’, ‘boolean’, ‘object’ and ‘string’.
What Now
That’s it really. I still use vanilla HTMLElement
custom elements, and in fact they are present on my project websites.
But for things that need reactivity with real JS values as properties… I have a tiny, minimalistic base class for that. :)
Check it out on GitHub. ✨