https://18alan.space/posts/how-hard-is-it-to-build-a-frontend-framework.html home about [logo] Building a Frontend Framework; Reactivity and Composability With Zero Dependencies 13th May, 2023 Before I start--to set some context--by frontend framework what I mean is, a framework that allows us to avoid having to write regular old HTML and JavaScript such as this:

and instead allows us to write magical HTML and JavaScript code such as this (Vue): or this (React): export default function Para() { const coolPara = 'Lorem ipsum'; return

{ coolPara }

; } and the benefit of such a framework is understandable. Remembering words or phrases such as document, innerText, and getElementById are difficult--so many syllables! Okay, syllable count isn't the main reason. Reactivity The first main reason is that, in the second and third examples, we can just set or update the value of the variable coolPara and the markup--i.e. the

element--is updated without without explicitly having to set its innerText. This is called reactivity, the UI is tied to the data in such a way that just changing the data updates the UI. Composability The second main reason is the ability to define a component and reuse it without having to redefine it every time we need to use it. This is called composability. Regular HTML + JavaScript does not have this by default. And so the following code does not do what it feels like it should:

Lorem ipsum. Reactivity and composability are the two main things the usual frontend frameworks such as Vue, React, etc give us. These abstractions aren't granted for free, one has to front-load a bunch of framework specific concepts, deal with their leakiness when things work in inexplicably magical ways, and not to mention, a whole load of failure-prone dependencies. But, it turns out that using modern Web APIs these two things aren't very hard to achieve. And most use cases we might not actually need the usual frameworks and their cacophony of complexities... Reactivity A simple statement that explains reactivity is when the data updates, update the UI automatically. The first part is to know when the data updates. This unfortunately is not something a regular object can do. We can't just attach a listener called ondataupdate to listen to data update events. Fortunately JavaScript has just the thing that would allow us to do this, it's called Proxy. Proxy Objects Proxy allows us to create a proxy object from a regular object: const user = { name: 'Lin' }; const proxy = new Proxy(user, {}); and this proxy object can then listen to changes to the data. In the example above we have a proxy object, but it is not really doing anything when it comes to know that name has changed. For that we need a handler, which is an object that tells the proxy object what to do when the data is updated. // Handler that listens to data assignment operations const handler = { set(user, value, property) { console.log(`${property} is being updated`); return Reflect.set(user, value, property); }, }; // Creating a proxy with the handler const user = { name: 'Lin' }; const proxy = new Proxy(user, handler); Now whenever we update name using the proxy object, we'll get a message saying "name is being updated". If you're wondering, What's the big deal, I could've done this using a regular old setter, I'll tell you the deal: * The proxy method is generalized, and handlers can be reused, which means that... * Any value you set on a proxied object can be recursively converted into a proxy, which means that... * You now have this magical object with the ability to react to data updates no matter how nested it is. Other than this you can handle several other access events such as when a property is read, updated, deleted, etc. Now that we have the ability to listen to listen to operations, we need to react to them in a meaningful way. Updating the UI If you recall, The second part of reactivity was update the UI automatically. For this we need to fetch the appropriate UI element to be updated. But before that that we need to first mark a UI element as appropriate. To do this we'll use data-attributes, a feature that allows us to set arbitrary values on an element:

The nicety of data-attributes are that we can now find all the appropriate elements using: document.querySelectorAll('[data-mark="name"]'); Now we just set the innerText of all the appropriate elements: const handler = { set(user, value, property) { const query = `[data-mark="${property}"]`; const elements = document.querySelectorAll(query); for (const el of elements) { el.innerText = value; } return Reflect.set(user, value, property); }, }; // Regular object is omitted cause it's not needed. const user = new Proxy({ name: 'Lin' }, handler); That's it, that's the crux of reactivity! Because of the general nature of our handler, for any property of user that is set, all the appropriate UI elements will be updated. That's how powerful the JavaScript Proxy features are, with zero dependencies and some cleverness it can give us these magical reactive objects. Now onto the second main thing... Composibility Turns out, browsers already have an entire feature dedicated to this called Web Components, who knew! Few use it cause it's a bit of a pain in the ass to use (and also because most reach out for the usual frameworks as a default when starting a project, irrespective of the scope). For composability we first need to define the components. Defining components using template and slot The