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.
The idea?:
Say you have the following piece of logic...
let count = 10;
let doubleCount = count * 2;
console.log(doubleCount);
a change to count
...
setTimeout(() => count = 20, 1000);
(being a change in program state) should be automatically reflected down the line such that with the above, the console has a second output of 40
.
This is that thing you want to do in React when you write:
import { useState, useMemo, useEffect } from 'react';
const [count, setCount] = useState(10);
const doubleCount = useMemo(() => count * 2, [count]);
useEffect(() => {
console.log(doubleCount);
}, [doubleCount]);
setTimeout(() => {
setCount(20);
}, 1000);
and in a Signals-based framework like Solid when you write:
import { createSignal, createMemo, createEffect } from 'solid-js';
const [ count, setCount ] = createSignal(10);
const doubleCount = createMemo(() => count() * 2);
createEffect(() => {
console.log(doubleCount());
});
setTimeout(() => {
setCount(20);
}, 1000);
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:
webqit
/
quantum-js
A runtime extension to JavaScript that enables us do Imperative Reactive Programming (IRP) in the very language!
Quantum JS
Overview • Creating Quantum Programs • Implementation • Examples • License
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 primitives
import { createSignal, createMemo, createEffect } from 'solid-js';
// Declare values
const [ count, setCount ] = createSignal(5);
const doubleCount = createMemo(() => count() * 2);
// Log this value live
createEffect(() => {
console.log(doubleCount());
});
// Setup periodic updates
setInterval(() => setCount(10), 1000);
Quantum JS lets you acheive the same in the ordinary imperative form of the language:
// Declare values
let count = 5
…Already, you can have much of what you see here working right in your browser via the Quantum JS script:
<script src="https://unpkg.com/@webqit/quantum-js/dist/main.js"></script>
Details right in the readme
Laying Reactive Primitives to Rest
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>
function program() {
// 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>
shouldbereactive function program() {
// 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...
<script quantum>
let products = [];
console.log(products.length, products[0]); // 0, undefined
</script>
each of the following operations is automatically reflected in the console:
products.push('T-Shirts');
// console: 1, 'T-Shirts'
products.pop();
// console: 0, undefined
Think reactive object mutations, wherein, given the below...
<script quantum>
let person = {};
console.log(person.name, person.age); // undefined, undefined
</script>
each of the following operations is automatically reflected in the console:
person.name = 'Sam';
// console: 'Sam', undefined
Object.assign(person, { name: 'Tam', age: 1 });
// console: 'Tam', 1
Object.defineProperty(person, 'name', { value: 'Ham' });
// console: 'Ham', 1
Think live counters:
<script quantum>
let count = 0;
console.log(count);
</script>
with each tick from below automatically reflecting in the console:
setInterval(() => count++, 1000);
Think live loops, wherein, given the below...
<script quantum>
let products = [];
for (let p of products) {
console.log(p);
}
</script>
each of the following operations automatically advances the iteration:
products.push('T-Shirts');
// console: 'T-Shirts'
products.push('Backpacks');
// console: 'Backpacks'
Think live flow control constructs, wherein, given the below...
let condition;
quantum function program() {
if (!condition) {
console.log('Exiting...');
return;
}
console.log('Running...');
}
program();
// console: 'Exiting...'
each of the following operations automatically controls the program flow:
condition = true;
// console: 'Running...'
condition = false;
// console: 'Exiting...'
condition = true;
// console: 'Running...'
Think, think: destructuring assignments, rest assignments, and more.
...and Beyond
Think of some weird things that could now be possible, like "live" module imports in JavaScript! (I.e. Reactivity through the wire!)
Here's a demo of that that works today:
<!-- exporting module -->
<script type="module" quantum id="module-1">
export let localVar = 0;
setInterval(() => localVar++, 500);
</script>
<!-- importing module -->
<script type="module" quantum>
import { localVar } from '#module-1'; // the ID of the module script above
console.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:
<script src="https://unpkg.com/@webqit/quantum-js/dist/main.js"></script>
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:
<script src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
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', class extends HTMLElement {
count = 10;
connectedCallback() {
// Initial rendering
this._state = this.render();
// Static reflection at click time
this.addEventListener('click', () => {
this.count++;
});
}
disconnectCallback() {
// Cleanup
this._state.dispose();
}
quantum render() {
let countElement = this.querySelector('#count');
countElement.innerHTML = this.count;
let doubleCount = this.count * 2;
let doubleCountElement = this.querySelector('#double-count');
doubleCountElement.innerHTML = doubleCount;
let quadCount = doubleCount * 2;
let quadCountElement = this.querySelector('#quad-count');
quadCountElement.innerHTML = quadCount;
}
});
</script>
<click-counter style="display: block; padding: 1rem;">
Click me<br>
<span id="count"></span><br>
<span id="double-count"></span><br>
<span id="quad-count"></span>
</click-counter>
Example 2: A Custom URL
API
└─────────
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>
const MyURL = class {
constructor(href) {
// The raw url
this.href = href;
// Initial computations
this.compute();
}
quantum compute() {
// These will be re-computed from this.href always
let { protocol, hostname, port, pathname, search, hash } = new URL(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;
let href = this.origin + this.pathname + this.search + this.hash;
if (href !== this.href) { // Prevent unnecessary update
this.href = href;
}
}
}
</script>
<script>
// Instantiate
const url = new MyURL('https://www.example.com/path');
// Change a property
url.protocol = 'http:'; //Observer.set(url, 'protocol', 'http:');
console.log(url.href); // http://www.example.com/path
// Change another
url.hostname = 'foo.dev'; //Observer.set(url, 'hostname', 'foo.dev');
console.log(url.href); // http://foo.dev/path
</script>
Example 3: An Imperative List
└─────────
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.)
<section namespace>
<!-- The "items" template -->
<template def="partials" scoped>
<li def="item"><a></a></li>
</template>
<!-- The list container -->
<ul id="list"></ul>
<script quantum scoped>
// Import item template
let itemImport = this.import('partials#item');
let itemTemplate = itemImport.value;
// Iterate
let items = [ 'Item 1', 'Item 2', 'Item 3' ];
for (let entry of items) {
const currentItem = itemTemplate.cloneNode(true);
// Add to DOM
this.namespace.list.appendChild(currentItem);
// Remove from DOM whenever corresponding entry is removed
if (typeof entry === 'undefined') {
currentItem.remove();
continue;
}
// Render
currentItem.innerHTML = entry;
}
// Add a new entry
setTimeout(() => items.push('Item 4'), 1000);
// Remove an new entry
setTimeout(() => 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 😅.
Much Thanks
...to everyone who gave great feedback on drafts of this post, including: Joe Pea, and Alex Russell!
Top comments (0)