DEV Community

Cover image for A Fresh Take On Reactivity
Oxford Harrison
Oxford Harrison

Posted on • Edited on

A Fresh Take On Reactivity

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);
Enter fullscreen mode Exit fullscreen mode

a change to count...

setTimeout(() => count = 20, 1000);
Enter fullscreen mode Exit fullscreen mode

(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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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:

GitHub logo webqit / quantum-js

A runtime extension to JavaScript that enables us do Imperative Reactive Programming (IRP) in the very language!

Quantum JS

npm version bundle License

OverviewCreating Quantum ProgramsImplementationExamplesLicense

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());
});
Enter fullscreen mode Exit fullscreen mode
// Setup periodic updates
setInterval(() => setCount(10), 1000);
Enter fullscreen mode Exit fullscreen mode

Quantum JS lets you acheive the same in the ordinary imperative form of the language:

// Declare values
let count = 5
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
function program() {
  // Program body:
  // 100s of lines of code here
}
Enter fullscreen mode Exit fullscreen mode

and express the additional intent of reactivity with only a declaration:

<script shouldbereactive>
  // Program body:
  // 100s of lines of code here
</script>
Enter fullscreen mode Exit fullscreen mode
shouldbereactive function program() {
  // Program body:
  // 100s of lines of code here
}
Enter fullscreen mode Exit fullscreen mode

of course, with shouldbereactive being actually the keyword quantum, in practice:

<script quantum>
  // Program body:
  // 100s of lines of code here
</script>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

each of the following operations is automatically reflected in the console:

products.push('T-Shirts');
// console: 1, 'T-Shirts'
Enter fullscreen mode Exit fullscreen mode
products.pop();
// console: 0, undefined
Enter fullscreen mode Exit fullscreen mode

Think reactive object mutations, wherein, given the below...

<script quantum>
  let person = {};
  console.log(person.name, person.age); // undefined, undefined
</script>
Enter fullscreen mode Exit fullscreen mode

each of the following operations is automatically reflected in the console:

person.name = 'Sam';
// console: 'Sam', undefined
Enter fullscreen mode Exit fullscreen mode
Object.assign(person, { name: 'Tam', age: 1 });
// console: 'Tam', 1
Enter fullscreen mode Exit fullscreen mode
Object.defineProperty(person, 'name', { value: 'Ham' });
// console: 'Ham', 1
Enter fullscreen mode Exit fullscreen mode

Think live counters:

<script quantum>
  let count = 0;
  console.log(count);
</script>
Enter fullscreen mode Exit fullscreen mode

with each tick from below automatically reflecting in the console:

setInterval(() => count++, 1000);
Enter fullscreen mode Exit fullscreen mode

Think live loops, wherein, given the below...

<script quantum>
  let products = [];
  for (let p of products) {
    console.log(p);
  }
</script>
Enter fullscreen mode Exit fullscreen mode

each of the following operations automatically advances the iteration:

products.push('T-Shirts');
// console: 'T-Shirts'
Enter fullscreen mode Exit fullscreen mode
products.push('Backpacks');
// console: 'Backpacks'
Enter fullscreen mode Exit fullscreen mode

Think live flow control constructs, wherein, given the below...

let condition;
Enter fullscreen mode Exit fullscreen mode
quantum function program() {
  if (!condition) {
    console.log('Exiting...');
    return;
  }
  console.log('Running...');
}
Enter fullscreen mode Exit fullscreen mode
program();
// console: 'Exiting...'
Enter fullscreen mode Exit fullscreen mode

each of the following operations automatically controls the program flow:

condition = true;
// console: 'Running...'
Enter fullscreen mode Exit fullscreen mode
condition = false;
// console: 'Exiting...'
Enter fullscreen mode Exit fullscreen mode
condition = true;
// console: 'Running...'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 😅.

Star Us on Github ★

Much Thanks

...to everyone who gave great feedback on drafts of this post, including: Joe Pea, and Alex Russell!

Top comments (0)