VanillaJS, the reactive web framework
Browsers have come a long way. IE is fading into the repressed memories of battle-scarred developers. You can now sidestep the complex build tools once necessary to transpile new language features for old browsers. Even without a framework, it's not very difficult to make reactive components with plain javascript nowadays.
Here is how I do it quick and dirty. The first secret is that I lied a bit and this is not completely vanilla. I do use a templating library, lit-html, which, while not providing reactiviy, does a lot of heavy lifting.
Lit-html is pretty close to the metal. The templates are plain html, interpreted directly by the browser. There is no JSX, no second html, no virtual dom which is more effecient specially since it uses native javascript template literals that translate into native browser html sprinkled with <template> tags. When rendering multiple times, it also recursively checks for changes in a diff like process which means that only what changes is re-rendered in between refreshes. You could also use another templating library you like. This is not what makes things reactive.
So let's create a simple javascript component using a plain javascript class.
After doing npm install lit-html in our static files directory we might write in a js file:
import {html, render} from '/node_modules/lit-html/lit-html.js'; class MainView{ constructor(){ this.el=document.createElement("div"); //root div for the component this.subcomponent=new SomeOtherView(); this.name='world'; //view property } render(){ this.subcomponent.somestate=this.somestate;//propagate state down if necessary. this.subcomponent.render(); //the template let h=html` <H1 @click=${this.changeName}>Hello ${this.name}</H1> ${this.subcomponent.el} `; render(h, this.el,{host:this}); } changeName(){ //click handler this.name='how are you?'; this.render(); } }
How can we improve this? Well first, using the pattern above, developers tend to call render() every time a bit of component state changes, which is often redundant and inefficient. Browsers won't show changes until the main thread returns from the code anyways so in most cases, it's sufficient to have a single render that happens just before the code execution returns control to the browser (the exception is when you need to read things like dimensions from the DOM in code but try to avoid this for efficiency). Modern browsers have a function window.queueMicrotask that allows running code just at the right moment. Here are some helper functions you might use:
refresh(){ if(this.refreshScheduled===false){ this.refreshScheduled=true; window.queueMicrotask(()=>this._update()); } } _update(){ this.render(); this.refreshScheduled=false; }
Every time things need to be re-rendered, just call this.refresh() instead of this.render() thus avoiding redundant renderings. I find this also fixes bugs that sometimes crop up with multiple renderings where a scrollbar might lose its position and jump around or reset to the top.
What if you don't want to manually call refresh() every time you change the state of your component? It's easy to add some javascript accessors that automatically call refresh() when a property changes. Here is a function that allows you to add get and set accessors by name:
property(name){ Object.defineProperty(this, name, { set(v){ if(this._properties[name]!==v){ this._properties[name]=v; this.refresh(); } }, get(){return this._properties[name];} }); }
With these helper functions for example, after calling this.property('somestate') in the constructor of SomeOtherView, this.subcomponent.somestate=this.somestate won't need to be followed by a call to this.subcomponent.render() or even refresh(), thus making the component reactive. State can be propagated recursively through subcomponent's render functions and will be refreshed automatically at the right time, when needed. The recursion stops when properties don't change so this is quite efficient.
You can put the above helper functions in a class. I also added some code in the constructor to help set up the properties.
class LittleLit { constructor(options) { options=options || {}; this.el=document.createElement("div"); this.refreshScheduled=false; this._properties={}; let properties=this.constructor.properties; if(properties!==undefined){ for (let prop in properties) { this.property(prop,properties[prop]); this[prop]=options[prop];//initialize properties from constructor options } } this.refresh(); } refresh(){ if(this.refreshScheduled===false){ this.refreshScheduled=true; window.queueMicrotask(()=>this._update()); } } _update(){ this.render(); this.refreshScheduled=false; } property(name,options){ //you could do something with options //like passing a different comparator function to detect change //instead of !== Object.defineProperty(this, name, { set(v){ if(this._properties[name]!==v){ this._properties[name]=v; this.refresh(); } }, get(){return this._properties[name];} }); } }
Here is our component using the above class:
class MainView extends LittleLit{ static properties = { name: {} //'name' property and its (empty) options }; constructor(){ super(); this.subcomponent=new SomeOtherView(); this.name='world'; } render(){ this.subcomponent.somestate=this.somestate;//will propagate state down and potentially render let h=html` <H1 @click=${this.changeName}>Hello ${this.name}</H1> ${this.subcomponent.el} `; render(h, this.el,{host:this}); } changeName(){ this.name='how are you?';//this may trigger a refresh; } }
We add a main function to our javascript to create the component
let main=function(){ let mainview=new MainView(); mainview.el=document.querySelector('#main'); //attach to a div inside the html };
Without forgetting to load the html's script tag as an ES6 module so that it can load the lit-html library.
<script type="module" src="js/mainview.js"></script>
See the result here.
One benefit is that if we later want to convert our quick and dirty component to the more full featured lit.dev web component framework, we can just subclass LitElement instead of LittleLit with only minimal modifications to the code needed as we are using the lit templating engine already.
This is what I used to build Sim CB. In a future post, I'll describe how to implement a url router in a few dozen lines of javascript.