About: Full-time web tooling dude making cool concept cars for the web. I work on frameworks, compilers, CLIs and browser APIs. Currently building the world's most advanced Reactive Programming runtime!
Joined:
Apr 3, 2020
A Fresh Take On Reactivity
Publish Date: Mar 5
2 0
It's been months of work mocking up a new runtime capability in JavaScript! The new capability lets us have reactivity as an entirely language-driven feature in JavaScript such that using the exact same imperative syntax, we can write programs that automatically reflect changes to state in fine-grained details.
all of which, however, require special metaphors for the job and ultimately result in a new problem: too many moving parts and so much of a Functional Programming exercise for UI development!
Sit long enough with all current approaches, you realize that there's only so much that's possible without deep language support for reactivity. It comes straight out that the language is its own best enabler of the idea.
Having itched a ton on this over the past few months, today, I am excited to introduce the underexplored programming paradigm here: Imperative Reactive Programming!
This is being developed in this little repo of mine and is ready to be taken for a spin:
Quantum JS is a runtime extension to JavaScript that brings Imperative Reactive Programming to JavaScript!
What's that?
Overview
Where you normally would require certain reactive primitives to express reactive logic...
// Import reactive primitivesimport{createSignal,createMemo,createEffect}from'solid-js';// Declare valuesconst[count,setCount]=createSignal(5);constdoubleCount=createMemo(()=>count()*2);// Log this value livecreateEffect(()=>{console.log(doubleCount());});
Reactivity has only been possible by means of functions. Reactive systems rely on use of these primitives to gain the ability to track state and drive effects.
By contrast, the language-based model doesn't have this limitation. That effectively renders the essence of these primitives obsolete!
Coincidentally, this is what would be expected as the natural evolution of reactivity: towards less and less of its mechanical moving parts, not more! (People aren't itching to have more!)
It's essentially the path we've been walking for some time now with compilers—Svelte, Vue, React, etc.! (For example, the React compiler now helps you eschew the manual use of useMemo() and useCallback().) Only, we'd know we've hit perfection when there isn't any more primitive to remove!
There's an old quote regarding this benchmark:
"Perfection is attained not when there is nothing more to add, but when there is nothing more to take away." — Antoine de Saint-Exupéry (1939)
And it turns out, people really want to see things zero down to "arbitrary JS":
"It’s building a compiler that understands the data flow of arbitrary JS code in components.
Is that hard? Yeah!
Is it possible? Definitely.
And then we can use it for so many things…" - sophie alpert
The new language-based model is what you see when you walk the compiler path to its end!
Putting the Language to Work in Fun Ways
The first notable thing about the new language-based approach is how you can literally write an arbitrary piece of code... say within a script or function...
<script>// Program body:// 100s of lines of code here</script>
functionprogram(){// Program body:// 100s of lines of code here}
and express the additional intent of reactivity with only a declaration:
<script shouldbereactive>// Program body:// 100s of lines of code here</script>
shouldbereactivefunctionprogram(){// Program body:// 100s of lines of code here}
of course, with shouldbereactive being actually the keyword quantum, in practice:
<script quantum>// Program body:// 100s of lines of code here</script>
providing a direct upgrade path from "regular" to "reactive" (and back)—with no changes at all in code.
Here, the quantum declaration opts you in to a new execution mode in the runtime—the "quantum" execution mode—in which the runtime stays sensitive to changes in its own state and automatically gets the relevant parts of the program re-executed.
Being this way a runtime extension (rather than a syntax extension), your involvement in the process is done here. The rest, and bulk, of the story continues within the runtime—and by the "person" that's most specialized in low level things: the runtime itself!
What's next is the many amazing implications that this has for us as developers!
Reactivity in the Full Range of the JS Language
Given a language-level reactive system, think the freedom to use the full range of the JS language without losing reactivity!
Think reactive array mutations, wherein, given the below...
<!-- importing module --><script type="module"quantum>import{localVar}from'#module-1';// the ID of the module script aboveconsole.log(localVar);// 1, 2, 3, 4, etc.</script>
Personally looking forward to how this evolves! For example, might this eventually come to having an import attribute?:
import{localVar}from'#module-1'with{live:true};
Up for a Spin!
It's been super fun building the Quantum JS compiler along with its runtime, and essentially living at the very edge of the JavaScript language—learning every nuance that exists therein! We're now ready to run in your browser!
The fastest way to try is via the Quantum JS script loaded from a CDN:
But, of course, that's just one of many ways—depending on use case. For example...
Quantum JS itself has no concept of the DOM, and, as such, no concept of HTML elements like <script>. You'd, instead, need to use the OOHTML implementation of Quantum JS for DOM-based integration:
That in place, here are some of my favourite examples which you can copy/paste and run directly in your browser (without a build step):
Example 1: A Custom Element-Based Counter └─────────
In this example, we demonstrate a custom element that works as a counter. Notice that the magic is in its Quantum render() method. Reactivity starts at connected time (on calling the render() method), and stops at disconnected time (on calling dispose)! (You can also find this example in the Quantum JS list of examples.)
<script>customElements.define('click-counter',classextendsHTMLElement{count=10;connectedCallback(){// Initial renderingthis._state=this.render();// Static reflection at click timethis.addEventListener('click',()=>{this.count++;});}disconnectCallback(){// Cleanupthis._state.dispose();}quantumrender(){letcountElement=this.querySelector('#count');countElement.innerHTML=this.count;letdoubleCount=this.count*2;letdoubleCountElement=this.querySelector('#double-count');doubleCountElement.innerHTML=doubleCount;letquadCount=doubleCount*2;letquadCountElement=this.querySelector('#quad-count');quadCountElement.innerHTML=quadCount;}});</script>
In this example, we demonstrate a simple replication of the URL API - where you have many interdependent properties! Notice that the magic is in its Quantum compute() method which is called from the constructor. (You can also find this example in the Quantum JS list of examples.)
<script>constMyURL=class{
constructor(href){// The raw urlthis.href=href;// Initial computationsthis.compute();}quantumcompute(){// These will be re-computed from this.href alwayslet{protocol,hostname,port,pathname,search,hash}=newURL(this.href);this.protocol=protocol;this.hostname=hostname;this.port=port;this.pathname=pathname;this.search=search;this.hash=hash;// These individual property assignments each depend on the previous this.host=this.hostname+(this.port?':'+this.port:'');this.origin=this.protocol+'//'+this.host;lethref=this.origin+this.pathname+this.search+this.hash;if (href!==this.href){// Prevent unnecessary updatethis.href=href;}}}</script>
In this example, we demonstrate a reactive list of things. Notice how additions and removals on the items array are statically reflected on the UI! (You can also find this example and its declarative equivalent in the OOHTML list of examples.)
<sectionnamespace><!-- The "items" template --><templatedef="partials"scoped><lidef="item"><a></a></li></template><!-- The list container --><ulid="list"></ul><script quantumscoped>// Import item templateletitemImport=this.import('partials#item');letitemTemplate=itemImport.value;// Iterateletitems=['Item 1','Item 2','Item 3'];for (letentryofitems){constcurrentItem=itemTemplate.cloneNode(true);// Add to DOMthis.namespace.list.appendChild(currentItem);// Remove from DOM whenever corresponding entry is removedif (typeofentry==='undefined'){currentItem.remove();continue;}// RendercurrentItem.innerHTML=entry;}// Add a new entrysetTimeout(()=>items.push('Item 4'),1000);// Remove an new entrysetTimeout(()=>items.pop(),2000);</script></section>
And No, We Aren't Done!
There's definitely a bunch of things to come! And there's definitely a bunch of questions to be answered too, including:
How does reactivity actually happen? What is the update model?
How bad, really, was the explicit approach to reactivity?
What does it look like building real world applications?
This post is the first in a series!
Our Star ☆ Button
Would you help our project grow and help more people find us by leaving us a star on github? We, too, have a star button 😅.