2.1.86 • Published 10 months ago

@webqit/reflex-functions v2.1.86

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

Reflex Functions

MotivationOverviewUsecasesDocumentationPolyfillGetting InvolvedLicense

Reflex Functions are a new type of JavaScript function that enables fine-grained Reactive Programming in the imperative form of the language - wherein reactivity is drawn entirely on the dependency graph of your own code!

This is an upcoming proposal! (Introducing Imperative Reactive Programming (IRP) in JavaScript!)

Motivation

Reactivity has hostorically relied on a lot of runtime techniques and compiler magics, has required much manual work, and overall, constituted a fundamental paradigm shift to how we build applications. Approaches have often eaten away at the idiomatic use of the language, taken a toll on performance, and fiendishly messed with our brain with tricky runtime behaviours!

This is discussed extensively in the introductory blog post

We realized that we could solve the Language of Reactivity down to plain "JavaScript" - in both the literal form and linear flow of the language, in a way that translates well to a native language feature! This is what we explore now as Reflex Functions!

An Overview

Imagine a function that works like any other function - e.g. accepts a number of arguments:

function calculate(a, b) {
  console.log('Operand #1:', a);
  console.log('Operand #2:', b);
  console.log('Total:', a + b);
}
let operand1 = 2;
let operand2 = 3;
calculate(operand1, operand2);
Operand #1: 2
Operand #2: 3
Total: 5

But has a special ability to statically "reflect" changes to its external dependencies - e.g. those arguments:

operand2 = 8;
reflect('b'); // "b" being what the function sees
Operand #2: 8
Total: 10

Giving you fine-grained reactivity at the precision of the individual expressions!

This is what you have of Reflex Functions, and here's how it works:

Reflex Functions have a distinguishing syntax: a double star notation.

function** calculate() {
  // Function body
}

See Formal Syntax for details.

Function body is any regular piece of code that should statically reflect changes to its external dependencies:

let count = 10; // External dependency
function** calculate(factor) {
  // Reactive expressions
  let doubled = count * factor;
  console.log(doubled);
}

Return value is a two-part array that contains both the function's actual return value and a special reflect function for getting the function to reflect updates:

let [ returnValue, reflect ] = calculate(2);
console.log(returnValue); // undefined
doubledreturnValue
20undefined

The reflect() function takes just the string representation of the external dependencies that have changed:

count = 20;
reflect('count');
doubled
40

Path dependencies are expressed in array notation. And multiple dependencies can be reflected at once, if they changed at once:

count++;
this.property = value;
reflect('count', [ 'this', 'property' ]);

Change Propagation

Reactivity exists with Reflex Functions where there are dependencies "up" the scope to respond to! And here's the mental model for that:

┌─ a change happens outside of function scope

└─ is propagated into function, then self-propagates down ─┐

Changes within the function body itself self-propagate down the scope, but re-running only those expressions that depend on the specific change, and rippling down the dependency graph!

Below is a good way to see that: a Reflex Function having score as an external dependency, with "reflex lines" having been drawn to show the dependency graph for that variable, or, in other words, the deterministic update path for that dependency:

Code with reflex lines

It turns out to be the very mental model you would have drawn if you set out to think about your own code! Everything works in just how anyone would predict it!

Plus, there's a hunble brag: that "pixel-perfect" level of fine-grained reactivity that the same algorithm translates to - which you could never model manually; that precision that means no more, no less performance - which you could never achieve with manual optimization; yet, all without working for it!

Usecases

Usecase: Reactive Custom Elements

Reactivity with Custom Elements has often relied on manual change-propagation techniques and, in some cases, custom syntaxes that themselves rely on a compile step! (Lit, for example, follows a "HTML as string" approach for DOM structure - albeit via JavaScript template strings!) But what if we could decouple behaviour and presentation and just write normal rendering logic and yet gain fine-grained reactivity on top of that?

This is one thing that Reflex Functions could help with!

Example 1:

Below is a custom element that has Reflex Function as its render() method. The render() method would be invoked only once and subsequent updates would happen via reflections:

customElements.define('click-counter', class extends HTMLElement {

  count = 10;
  connectedCallback() {
    // Full rendering at connected time
    // meaning that the querySelector() calls in there are run as normal
    let [ , reflect ] = this.render();

    // Reflex actions at click time
    // this time, meaning that the querySelector() calls in there don't re-run
    this.addEventListener('click', () => {
      this.count ++;
      reflect([ 'this', 'count' ]);
    });
  }

  **render() {
    let countElement = document.querySelector( '#count' );
    countElement.innerHTML = this.count;
    
    let doubleCount = this.count * 2;
    let doubleCountElement = document.querySelector( '#double-count' );
    doubleCountElement.innerHTML = doubleCount;
    
    let quadCount = doubleCount * 2;
    let quadCountElement = document.querySelector( '#quad-count' );
    quadCountElement.innerHTML = quadCount;
  }

});

Example 2:

Below is a repeat of the example above; this time showing how the Observer API may be used to automatically drive updates into the render function:

customElements.define('click-counter', class extends HTMLElement {
  
  count = 10;
  connectedCallback() {
    // Full rendering at connected time
    // meaning that the querySelector() calls in there are run as normal
    let [ , reflect ] = this.render();

    // Using the Observer API to automatically drive updates into the render function
    Observer.observe(this, changes => {
      changes.forEach(change => reflect([ 'this', change.key ]));
    });

    // Reflex actions at click time
    // this time, meaning that the querySelector() calls in there don't re-run
    this.addEventListener('click', () => {
      this.count ++;
    });

  }

  **render() {
    let countElement = document.querySelector( '#count' );
    countElement.innerHTML = this.count;
    
    let doubleCount = this.count * 2;
    let doubleCountElement = document.querySelector( '#double-count' );
    doubleCountElement.innerHTML = doubleCount;
    
    let quadCount = doubleCount * 2;
    let quadCountElement = document.querySelector( '#quad-count' );
    quadCountElement.innerHTML = quadCount;
  }

});

The above is possible with the polyfills today with only a few modifications:

  1. The above double star syntax isn't supported as-is in JavaScript, but you could acheive the same using the ReflexFunction constructor as below:

    customElements.define('click-counter', class extends HTMLElement {
      constructor(href) {
        const [ , reflect ] = this.render.call(this); // notice the .call(this)
      }
    
      render = ReflexFunction(`
        // code here
      `);
    });

    Or you can check out the PlayElement mixin below.

  2. The literal update expression this.count++ isn't reactive as-is in JavaScript, but you can acheive the same using the Observer API's mutation methods:

    Observer.set(this, 'count', this.count + 1);

    Or you can "pre-transform" the count property to a reactive property:

    customElements.define('click-counter', class extends HTMLElement {
      connectedCallback() {
        Observer.accessorize(this, [ 'count' ]);
      }
    });

Example 3:

Below is how the PlayElement Custom Element mixin takes this concept further to bring Reflex-based reactivity to Custom Elements:

customElements.define( 'count-element', class extends PlayElement( HTMLElement ) {
  // List of methods that should be transformed to "reflex" functions
  static get reflexFunctions() {
    return [ 'render' ];
  }

  count = 10;
  connectedCallback() {
    this.addEventListener('click', () => {
      this.count ++;
    });
  }

  render() {
    let countElement = document.querySelector( '#count' );
    countElement.innerHTML = this.count;
    
    let doubleCount = this.count * 2;
    let doubleCountElement = document.querySelector( '#double-count' );
    doubleCountElement.innerHTML = doubleCount;
    
    let quadCount = doubleCount * 2;
    let quadCountElement = document.querySelector( '#quad-count' );
    quadCountElement.innerHTML = quadCount;
  }

});

Visit PlayElement

Usecase: Compile Target

Custom template languages have been designed to support reactivity on the frontend! (Sometimes extending JavaScript with XML-like syntaxes (JSX), and sometimes extending HTML with special directives (ngIf, v-if, {#each}{/each}, etc.) to support various things like data binding, event handling, conditional rendering, looping, and more!) But what if, instead of re-inventing a new language, we could just write conventional JavaScript as template language and gain fine-grained reactivity on top of that?

You could simply have Reflex Functions as your compile target!

Example 1:

Below is how the <script reflex> element in the OOHTML suite brings Reflex-based reactivity to HTML - by simply compiling to Reflex Functions under the hood:

<div>
  <script reflex scoped>
    console.log(this) // div

    console.log(this.liveProperty) // live expression

    if (this.bindings.isCollapsed) {
        // Live block
        console.log('Section collapsed!');
    }
  </script>
</div>

As a plus, the external dependencies within the script are automatically observed and reflected:

// Mutate a binding:
divElement.bindings.isCollapsed = true; // Console: "Section collapsed!"

Visit OOHTML

Usecase: Pure Computations

Reactivity isn't all a UI thing! Sometimes we find ourself elsewhere manually wiring callbacks to model depencencies that need to stay in sync! (And often, this takes a toll on readability and ergonomics!) But what if we could sometimes just express the logic in its static form and turn on reactivity on top of it? (Much like an escape hatch out of complexity :))

Consider some of these pure computational usecases!

Example 1:

Below is a simple way to implement something like the URL API - where you have interdependent properties! Reflex Functions just lets you express the logic and has it binding automatically:

class Url {

  constructor(href) {
    // The raw url
    this.href = href;
    // Initial computations
    const [ , reflect ] = this.compute();

    // Detect updates and reflect them
    Observer.observe(this, changes => {
      changes.forEach(change => reflect([ 'this', change.key ]));
    });

  }

  **compute() {
    // These will be re-computed from this.href always
    let [ protocol, hostname, port, pathname, search, hash ] = parseUrl(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;
    }
  }

}
// Change a property and have it's dependents auto-compute
const url = new Url('https://www.example.com/path');

url.protocol = 'http:';
console.log(url.href); // http://www.example.com/path

url.hostname = 'foo.dev';
console.log(url.href); // http://foo.dev/path

Example 2:

Below is a repeat of the example above; this time showing how we could take advantage of the Observer API's batching feature to batch updates and be even more performant:

class Url {

  constructor(href) {
    // The raw url
    this.href = href;
    // Initial computations
    const [ , reflect ] = this.compute();

    // Detect updates and reflect them
    Observer.observe(this, changes => {
      // All the updates from batch() below will now also be reflected as one batch
      let paths = changes.map(change => ([ 'this', change.key ]));
      reflect(...paths);
    });

  }

  **compute() {
    // These will be re-computed from this.href always
    let [ protocol, hostname, port, pathname, search, hash ] = parseUrl(this.href);

    // We batch the operations here so that they're delivered and reflected above as one batch
    Observer.batch(this, () => {
      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;
    }
  }

}
// Change a property and have it's dependents auto-compute
const url = new Url('https://www.example.com/path');

url.port = 1914;
console.log(url.href); // https://www.example.com:1914/path

url.pathname = '/level1/level2';
console.log(url.href); // https://www.example.com:1914//level1/level2

The above is possible with the polyfills today with only a few modifications:

  1. The above double star syntax isn't supported as-is in JavaScript, but you could acheive the same using the ReflexFunction constructor as below:

    class Url {
      constructor(href) {
        const [ , reflect ] = this.compute.call(this); // notice the .call(this)
      }
    
      compute = ReflexFunction(`
        // code here
      `);
    }

  2. The literal update expressions like this.href = href aren't reactive as-is in JavaScript, but you can acheive the same using the Observer API's mutation methods:

    Observer.set(this, 'href', href);

    Or you can "pre-transform" the properties to reactive properties:

    customElements.define('click-counter', class extends HTMLElement {
      connectedCallback() {
        Observer.accessorize(this, [ 'protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host', 'origin', 'href' ]);
      }
    });

Example 3:

Check out how the ReflexFunction.inspect() method ties in with the Observer API!

Documentation

Visit the docs for details around Formal Syntax, Heuristics, Flow Control and Functions, API, etc.

The Polyfill

Reflex Functions is being developed as something to be used today - via a polyfill. The polyfill features a specialized compiler and a small runtime that work together to enable all of Reflex Functions as documented, with quite a few exceptions. Known limitations are in the area of syntax, and these can be found in the relevant parts of the docs.

<script src="https://unpkg.com/@webqit/reflex-functions/dist/main.js"></script>

This is to be placed early on in the document and should be a classic script without any defer or async directives:

47.8 kB min + gz | 167 KB min

// Destructure from the webqit namespace
const { ReflexFunction } = window.webqit;
// npm install
npm i @webqit/reflex-functions
// Import API
import { ReflexFunction } from '@webqit/reflex-functions';

The current polyfill only supports the constructable form of Reflex Functions:

// External dependency
globalThis.externalVar = 10;

// Initial run
let sum = ReflexFunction( `a`, `b`, `return a + b + externalVar;` );
let [ result, reflect ] = sum(10, 10); // 30

// Reflections
externalVar = 20;
result = reflect( 'externalVar' ); // 40

But the double star syntax is supported from within the function itself:

const reflex = ReflexFunction(`
  // External dependency
  let externalVar = 10;

  // Initial run
  function** sum( a, b ) {
    return a + b + externalVar;
  }
  let [ result, reflect ] = sum( 10, 10 ); // 30

  // Reflections
  externalVar = 20;
  result = reflect( 'externalVar' ); // 40
`);
reflex();

Reflex Functions Lite

It is possible to use a lighter version of Reflex Functions where the bundle size of the main build above will impact initial application loading. The Lite version initially comes without the compiler and yet let's you work with Reflex Functions ahead of that.

This lazy-loading strategy also means that the Reflex Functions API will only be available in async mode! (This async mode is what makes it possible to load the compiler lazily!) And it comes with an additional perk: the compiler is loaded into a Web Worker and all compilations are able to happen off the main thread!

<script src="https://unpkg.com/@webqit/reflex-functions/dist/lite.js"></script>

This is to be placed early on in the document and should be a classic script without any defer or async directives:

// Destructure from the webqit namespace
const { ReflexFunction: ReflexFunctionLite } = window.webqit;
// npm install
npm i @webqit/reflex-functions
// Import Lite API
import { ReflexFunctionLite } from '@webqit/reflex-functions';

Reflex Functions Lite is an async API:

// External dependency
globalThis.externalVar = 10;

// Initial run
let sum = ReflexFunctionLite( `a`, `b`, `return a + b + externalVar;` );
let [ result, reflect ] = await sum(10, 10); // 30

// Reflections
externalVar = 20;
result = await reflect( 'externalVar' ); // 40

But being that the Reflex Functions Compiler is designed as a movable peice, it is all still possible to explicitly and synchronously load it alongside the Lite script - thus acheiving the exact same thing about the main build above, including being usable in sync mode.

<head>
  <script src="https://unpkg.com/@webqit/reflex-functions/dist/compiler.js"></script> <!-- Must come before the polyfil -->
  <script src="https://unpkg.com/@webqit/reflex-functions/dist/lite.js"></script>
</head>

Getting Involved

All forms of contributions are welcome at this time. For example, syntax and other implementation details are all up for discussion. Also, help is needed with more formal documentation. And here are specific links:

License

MIT.

2.1.69

10 months ago

2.1.67

10 months ago

2.1.68

10 months ago

2.1.66

10 months ago

2.1.85

10 months ago

2.1.63

10 months ago

2.1.86

10 months ago

2.1.64

10 months ago

2.1.83

10 months ago

2.1.84

10 months ago

2.1.81

10 months ago

2.1.82

10 months ago

2.1.80

10 months ago

2.1.78

10 months ago

2.1.79

10 months ago

2.1.76

10 months ago

2.1.77

10 months ago

2.1.74

10 months ago

2.1.75

10 months ago

2.1.72

10 months ago

2.1.73

10 months ago

2.1.70

10 months ago

2.1.71

10 months ago

2.1.62

11 months ago

2.1.61

11 months ago

2.1.60

11 months ago

2.1.59

11 months ago

2.1.58

11 months ago

2.1.57

11 months ago

2.1.56

11 months ago

2.1.55

11 months ago

2.1.54

11 months ago

2.1.53

11 months ago

2.1.52

11 months ago

2.1.51

11 months ago

2.1.50

11 months ago

2.1.49

11 months ago

2.1.48

11 months ago