Reactive State for Data & DOM

Photo by Marco Lermer on Unsplash

Update

all patterns described in this post have been merged into a reactive-props module that can be configured for any use case

Some pattern can be handy, without requiring heavy libraries or frameworks!

This post is a walk through reactive approaches and all the details to consider in adopting one or another.

The property Accessor

The first thing we need to understand is how the reactive paradigm works, which can be summarized as “some data changed, some update is needed”.

const data = {key: 'some value'};
// how can we intercept the following?
data.key = 'some other value';

A way to intercept any property change within, almost, any generic object, is to describe an “accessor”.

{
get(){}, // optional
set(value){}, // also optional
enumerable, // false by default
configurable // false by default
}

The method used to do so, is either Object.defineProperty(obj, key, descriptor) or Object.defineProperties(obj, descriptors).

Differently from generic descriptors, the writable configuration is not usable for accessors and vice-versa: writable properties can’t be accessors.

To quickly show where our code can sit, in order to react to a change, all we need to do is to use one of the two Object methods, and intercept a setter:

let value = void 0;
const data = Object.defineProperty({}, 'key', {
get() { return value; },
set(newValue) {
value = newValue;
console.log('react to changes!');
}
});

After that, data.key = 'any value' will trigger that setter, update the old value, and finally log “react to changes!”.

Great, we already have everything we need to react on data changes!

A basic Accessor

Note: with reactive-props module this pattern is covered by:

const basicHandler = reactiveProps({all: true});

As simple as it looks, the basicAccessor already solves tons of use cases, most notably those that need to react/update anything per each property change.

const basicAccessor = (value, update) => ({
get: () => value,
set: _ => {
value = _;
update();
}
});

To create an object that invokes some update() per each property though, we need some extra utility like this one:

const {defineProperties, keys} = Object;const basicHandler = (props, update) => {
const desc = {};
for (const key of keys(props))
desc[key] = basicAccessor(props[key], update);
return defineProperties({}, desc);
};

The basicHandler accepts some generic object literal to setup all accessors, and returns a special copy that will react to any change:

const state = basicHandler(
{test: ''},
() => console.log('updated')
);
state.test = 'OK'; // updated
state.test = 'OK'; // updated
state.test = 'OK'; // updated

That’s it, we now have few lines of code to create states that will invoke a generic callback, as soon some part of such state was set, even if the property that changed received exact same value … but can we do any better?

A better Accessor

Note: with reactive-props module this pattern is covered by:

const reactOnChanges = reactiveProps();

There are tons of cases where we don’t want, or need, to react to changes, specially if the update function would trigger a whole layout reflow, repaint, or computation, for something that was previously identical.

For these very common cases, at least in the UI side, we could write this:

const betterAccessor = (value, update, all) => ({
get: () => value,
set: _ => {
if (all || value !== _) {
value = _;
update();
}
}
});

The main difference is that now we have a “notify all” or “notify only changes” flag in the equation, but how can we pass such flag if any state change operation is simply a “set property value”?

state.change = value;

To differentiate between states that should trigger updates, regardless if the value is the same, we need to create different states handlers:

const {defineProperties, keys} = Object;const betterHandler = ({all = false} = {}) =>
(props, update) => {
const desc = {};
for (const key of keys(props))
desc[key] = betterAccessor(props[key], update, all);
return defineProperties({}, desc);
};

This betterHandler now returns a function that would carry along the all boolean option, so that states created through one handler could react to all, and other handlers could react only to actual changes.

const reactOnChanges = betterHandler();
// all option is false by default
const state = reactOnChanges(
{test: ''},
() => console.log('updated')
);
state.test = 'OK'; // updated
state.test = 'OK';
state.test = 'OK';
state.test = 'new'; // updated
state.test = 'new';

Awesome: without bloating the code, we have a way to create two kind of “data reactors” … but why is that important?

Different, shallow, use cases

Adding accessors to any field/property means that, instead of just retrieving, or overwriting, some value, callbacks are involved in the process, namely the get and the set(value) one, plus the eventual update() cost.

Since we do this only on the top level of the object though, mostly to keep it fast and simple, we don’t know if the data passed along represents actually the same data we had before.

Here an example:

const a = {data: [1, 2, 3]};
const b = {data: [1, 2, 3]};
state.data = a;
state.data = b;

Specially when it comes to network requests and JSON parsing, the data object is different each time, but if we have data stored somewhere else, and only one part of its content changed, we’ll be in trouble by not invoking any update() on such data change, even if it’s the same reference:

const same = {data: [1, 2, 3]};
state.data = same;
// later on ...
same.data.push(4);
state.data = same;
// no update triggered!!!

Accordingly, when shared data is meant to be passed around, it’s a good idea to make our descriptor less restrictive:

const betterAccessor = (value, update, all) => ({
get: () => value,
set: _ => {
if (
all ||
(typeof value === 'object' &&
value !== null) ||
value !== _
) {
value = _;
update();
}
}
});

At this point, we might as well drop the all flag and keep it even more simple, as the guard against “exact same data” and “possibly different data” is already builtin within the descriptor we have per each property.

I’ll let you decide if all flag is needed or not, once the simpleDescriptor implements the guard I’ve just described, but it’s surely reasonable to consider all these cases and be prepared to avoid surprises such as missed, or too many, updates.

With reactive-props module, pass {shallow: false} if the passed data is immutable.

A hooked Accessor

Note: with reactive-props module this pattern is covered by:

const useStateHandler = reactiveProps({useState});

While “hooks” are not strictly necessary to have reactive changes, when these are around, the easiest primitive to use is likely the useState one, provided by basically every hooks based library out there.

Flagging all might still be a good idea, but it’s just one of the options we could have to configure our “reactive states” upfront.

const useStateAccessor = (value, update, all) => ({
get: () => value,
set: _ => {
if (all || value !== _)
update(value = _);
// note the value is passed
}
});

The main difference with this accessor, is that value is passed as update(value) argument, so that the next, common, pattern could work:

const [counter, update] = useState(0);
update(counter + 1);

The dance needed to use hooks, still passing through state, is now this one:

const {defineProperties, keys} = Object;const useStateHandler = ({all = false, useState} = {}) =>
props => {
const desc = {};
for (const key of keys(props)) {
const [value, update] = useState(props[key]);
desc[key] = useStateAccessor(value, update, all);
}
return defineProperties({}, desc);
};

The value would be still passed along via a get() invoke, while the update(newValue) part will be delegated to the set(value) invoke, still keeping the value in sync within its own closure.

At this point, to create a “reactive” function, all we need to do is to pass the useState utility, commonly provided by a plethora of libraries:

// not a real useState utility
const useState = oldValue => [
oldValue,
newValue => {
console.log(`the value is now: ${newValue}`);
}
];
const reactive = useStateHandler({useState});const state = reactive({test: ''});
state.test = 'OK'; // the value is now: OK
state.test = 'OK';
state.test = 'OK';
state.test = 'new'; // the value is now: new
state.test = 'new';

Great, with 20 lines of JS we are now hooked into anything 🎉

But how does that actually work?

The reason it works is that usually properties to react for, are well known and static, but as useState, used conditionally, is kinda forbidden, from most “hooked” solutions, the following pattern looks like a very bad idea:

// foreign object might have a different number
// of properties each time, and this is an issue!
const state = reactive({...foreign, test: ''});

If you’d like to better understand why that could break, feel free to read this good ’ol post of mine regarding hooks 😉

About the DOM

Now that we know pretty much everything that needs to be known regarding data sate handling, it’s time to switch into a slightly more complicated topic.

There are two suns

Possibly, the most misunderstood part of the HTML specification, is that HTML has no own properties concept, it’s just based on attributes, that either are there, have a value, or don’t exist.

<p attr="value">OK</p>
<p attr>also OK</p>
<p>still OK</p>
  • the first p has an attr attribute with value value, which is also always a string.
  • the second p has an attr attribute, but its value is empty string, even if its meaning is boolean (i.e. <button disabled>)
  • the third p has no attr at all

However, if we attach directly a property, instead of an attribute, to any of those p elements, none of their values would be reflected as attribute, unless their prototype has some special meaning for that property, mapped somehow to a DOM view update (aka: the DOM also reacts to properties changes)

const p = document.createElement('p');
p.some = 'thing';
p.outerHTML; // "<p></p>"
p.hidden = true;
p.outerHTML; // "<p hidden=\"\"></p>"

It is extremely important to get this distinction between direct properties access, and attributes, as the following implications applies:

  • an attribute change easily results into a potential CSS change too, if we mistake properties for attributes, the browser might end up doing much more than we were hoping for
  • a well known property might have visual reactions once the property is reflected on the DOM and the risk is that our own accessors might shadow the native DOM behavior, for better or worse
  • in Custom Elements world, observing attributes requires that developers use element.setAttribute(key, value) also always resulting into value being, and the attributeChangedCallback receive, a string, so that the interaction becomes less natural with regular JS world, where properties can usually address any kind of value, while attributes can handle only strings
  • setting an attribute or using its DOM accessor, doesn’t often produce the expected result
const input = document.createElement('input');// as attribute
input.setAttribute('value', 'before');
input.outerHTML;
// <input value="before">
// as accessor
input.value = 'after';
input.outerHTML;
// still <input value="before">
// but ...
input.value;
// "after"

Accordingly, whenever we see <el attr="value">, we are seeing an attribute set as string that is visible on the UI/CSS/DOM, while el.attr = 'value' won’t be reflected, unless the attribute is a special one that also reacts on the view, as most boolean attributes are, but also indirectly the style one.

Why does this matter?

It matters because if your library of choice doesn’t have such distinction between what’s being set as attribute, and what’s instead an elemenet property, such library will likely cause more headaches, and UI related performance issues, than you might expect. For instance:

  • a library that reflects properties changes as attributes, might cause unnecessary rendering operations, as any CSS selector could have a div[data] { do: something; } in it, so that every attribute change could interfere with such selector, resulting into more browsers operations
  • a library that consider attributes as properties, will always be limited to string values, as nothing else can be represented on the HTML side
  • a library that doesn’t do this distinction explicitly, is just ambiguous

SSR though …

So how would we pick up some property that maybe it’s just fine as a string, or we are sure it represents some JSON value that could be parsed?

Since Server Side Rendering can only produce HTML, we need to take into account possible values, the same way native JS/DOM interaction does … so, back to the input example:

const input = document.createElement('input');
input.setAttribute('value', 'before');
input.value; // "before"

It’s only when we directly set the input property that it’ll be detached from its HTML representation, but since any server, or static page, could render <input value="before">, there is a simplification of the accessor, so that if not directly set, the returned value is the attribute, not the property.

An element Accessor

Note: with reactive-props module this pattern is covered by:

const elementHandler = reactiveProps({dom: true});

OK, now we know everything that needs to be known between properties and attributes, so that the following code shouldn’t be too surprising:

const {defineProperties, keys} = Object;const elementHandler = ({all = false} = {}) =>
(element, props, update) => {
const desc = {};
for (const key of keys(props)) {
let value = props[key];
if (element.hasOwnProperty(key)) {
value = element[key];
delete element[key];
}
else if (element.hasAttribute(key))
value = element.getAttribute(key);
desc[key] = betterAccessor(value, update, all);
}
return defineProperties(element, desc);
};

Few key-takes here:

  • if the element.property = value was previously set, the initial accessor value should use it, as it’s more meaningful than the default, base, property passed as props
  • the same applies for an element that comes from HTML with an attribute, and the same property name, already defined, as it was for the previous <input value="initial"> example, remember? In such case, it’s OK to use that attribute value as accessor initial value, because likely, if it was already represented as HTML, it means that value is probably a string anyway, but be aware that numbers and booleans might need a JSON.parse(attribute) if the expected property has a different type from string, so that the rest of the code will work as expected
  • in any other case, the initial accessor value is simply the props[key], intended as default value, or fallback

Worth mentioning, probably slightly late in this paragraph, that the betterAccessor here is recycled from the top of this post, but it also could be any other mentioned accessors’ utils, as here we’re focusing on the state handler logic … now, this is the final result:

const reactiveElement = elementHandler();const body = reactiveElement(
document.body,
{test: ''},
() => {
console.log('body updated');
}
);
body.test = 'OK'; // body updated
body.test = 'OK';
body.test = 'OK';
body.test = 'new'; // body updated
body.test = 'new';

And there it is, a reactive state attached directly to any DOM element, so that Custom Elements, as well as any other regular element, could have it’s own state handling represented by properties, not attributes!

To keep in mind

Even if Custom Elements were created to deal with any sort of attribute change, listed via the observedAttribute static array, which accepts any attribute name, whenever we are using properties, as opposite of attributes, we should be careful to not risk future-proof compatibility issues.

It is true that Web standards are not exactly the fastest moving piece in technology these days, when it comes to new proposals and their adoption, yet if we attach properties named with already available attributes in other nodes, or those that have special meanings, we might compromise “the future Web” (so please don’t name your property accessor like el[aria-case])

A word about libraries

In the React JSX case, which is a (sort of) HTML facade, it’s common to see what apparently looks like attributes, yet used as properties:

function Comp() {
return (
<Sub key={value} />
);
}

But even if key looks like a legit attribute, the Sub component will not receive it as attribute, but as a props object, so that props.key will have value, not the node it represents.

Yet in React, not all props are reflected as attribute, it’s a developer choice to do so, so that the distinction here is important, as JSX might mislead, if interpreted as plain/raw HTML.

In other template literals based libraries, it’s also crucial to understand how these work underneath.

As example, µhtml does a clear distinction between what’s meant as attribute, and what’s meant as JS property accessor, through the explicit, not HTML standard, . prefix, so that:

html`<element attr=${str} .prop=${value} />`

means that the element will have an attribute arr with a string content, but also an element.prop what will carry whatever kind of value it is, not only strings, with or without accessors around.

Why is this important?

When it comes to Custom Elements, combined with reactive patterns, knowing that an attribute could trigger an attributeChangedCallback, instead of an update via hooks, or any other technique shown in this post, is extremely important, simply because the attribute change won’t pass through the setter dance, it will be passed along as attribute change and string, and unless there is an observer that pass that attribute through the state handler, nothing will work as expected.

class MyCE extends HTMLElement {
static observedAttributes = ['prop'];
constructor() {
super();
const update = this.update.bind(this);
reactiveElement(this, ['prop'], update);
}
attributeChangedCallback(key, prev, value) {
this[key] = value;
}
update() {
this.textContent = this.prop;
}
}

Above Custom Element class is an example of a component that could accept both <el prop=${value}> and <el .prop=${value}>, but it’s still better to make this distinction less ambiguous at the library level, simply because the former <el prop=...> version would always pass a string, while the property could be of any other kind, so that if the rest of the code expects el.prop to return a boolean, a number, or some complex data, it can’t.

Why not using a Proxy?

As I’ve been recently asked about this, I think it’s worth answering in here as well. There are various reasons for not using a Proxy:

  • compatibility: Proxy won’t work in IE11 or legacy browsers. Current reactive-props module works down to IE9 or IE8 in the dom handler case
  • performance: a Proxy can operate on any key, not just those predefined, but as previously explained, if there is a useState pattern, this must know all properties upfront and it must guard against undesired properties access. The sum is: Proxy as wrapper and extra checks needed per each property accessed
  • not DOM friendly: a Proxy wraps the reference its proxying, which might be fine with regular states, but it won’t produce the desired effect with DOM elements, because only the proxy creator can send, or pass around, the proxy, but the element itself won’t be upgraded, so its state won’t be reflected across the rest of the stack.

As summary, since reading and setting state properties is in the critical path, and since elements should be upgraded, not just wrapped, the Proxy is not a solution to this problem, or better, it’s not suitable for performance and portability, and it won’t work with useState and elements.

If we don’t care about any of these constraints, and we’d like to just react on any change, this could be an option too:

const proxyHandler = (props, update) => new Proxy(props, {
set: (props, key, value) => {
props[key] = value;
update(key, value);
return true;
}
});
const state = proxyHandler({test: ''}, console.log);
state.test = 'OK';
// test OK
state.any = 'still OK';

To guard against not known properties, the handler would look like this:

const proxyHandler = (props, update) => new Proxy(props, {
set: (props, key, value) => {
const result = props.hasOwnProperty(key);
if (result) {
// <optional>
// if (props[key] !== value) {
props[key] = value;
update(key, value);
// }
}
return result;
}
});

Some Live Example

This wickedElements demo, together with this µce based one, use these patterns, without needing hooks, to update everything on state change, so it’s possible to see, and play, for real, with these tiny helpers we’ve discussed.

Conclusion

I believe there are a lot of sub-topics rarely touched in Web related technical posts, and I hope there’s some food for thoughts on both state handling patterns, their potentials, the fact we don’t really need React to have hooks like functionality, and the fact there’s a need to talk more about attributes VS properties, when it comes to DOM elements.

Last, but not least, my apologies for the longer-than-usual post, but I think it’s a good reference to have around, hoping there was something to learn too. 👋

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store