Simple tiny dependency graph engine, MobX inspired

Overview

๐Ÿœ‰ Quarx

Simple tiny dependency graph engine, MobX inspired

Introduction

In less than 200 lines of code and zero dependencies Quarx supports most of MobX core functionality:

  • createAtom, autorun are the low-level core primitives of the Quarx reactive engine
  • computed, observable.box are built on top of those primitives
  • all of the above behave the same way as their MobX equivalents

Unlike MobX, Quarx does not support circular computations even if they might eventually settle. This deliberate design decision allowed for dramatic algorithm simplification while circular calculation graphs do little to promote code clarity.

Another difference with MobX, and the primary reason Quarx saw the light of day is that Quarx always runs the computation immediately and synchronously when autorun is called, while MobX always delays the execution of nested reactions until the parent reaction exits.

With greedy execution, one can create new observed atoms on the fly (from within a reaction), paired with an autorun that should synchronously hydrate the atom at creation. This is by the way exactly how computed is implemented in Quarx.

Usage example

import { autorun, computed, observable, batch } from 'quarx';

const a = observable.box(1);
const b = observable.box(2);
const a_plus_b = computed(() => a.get() + b.get());

console.log('Initial calculation');
autorun(() => console.log(`a + b = ${a_plus_b.get()}`));

console.log('First update');
batch(() => {
  a.set(5);
  b.set(6);
});

console.log('Second update');
batch(() => {
  a.set(4);
  b.set(7);
});

// *** Prints ***
// Initial calculation
// a + b = 3
// First update
// a + b = 11
// Second update

Low-level concepts

There are 2 core primitive abstractions in Quarx: computations and atoms.

A computation is simply a thunk - a parameterless function. Computations are linked together into a DAG (directed acyclical graph) using atoms: simple interfaces created using the low-level createAtom API function:

  • A computation that calls atom.reportObserved() becomes the atom's downstream observer
  • A computation that calls atom.reportChanged() becomes the atom's upstream dependency
  • When atom.reportChanged() is called, all atom's downstream observers are scheduled for re-calculation
  • When atom.reportObserved() is called, it first makes sure that all computations in the atom's upstream sub-graph are up to date before returning control
  • autorun(computation) creates a computation, immediately executes it, and subsequently re-runs it each time any of the atoms reported observed during the last execution change

During a single synchronous re-actualization (hydration) run of the DAG each computation would be executed at most once.

  type Disposer = () => void;

  interface Atom {
    reportObserved: () => boolean;
    reportChanged: () => void;
  }

  export interface CoreOptions {
    name?: string;
    onError?: () => void;
  }

  export function createAtom(onBecomeObserved?: () => Disposer | void, options?: CoreOptions): Atom;
  export function autorun(computation: () => void, options?: CoreOptions): Disposer;

  export function batch(changes: () => void): void;
  export function untracked<T>(fn: () => T): T;

NOTE

If passed, the onBecomeObserved will be called the first time the atom becomes observed by a downstream computation. If it returns a function, this latter will be called when all the observers unsubscribe from the atom.

autorun returns a disposer which will unsubscribe the computation from all its currently observed atoms. This makes it a perfect candidate to return from onBecomeObserved: this will propagate the subscription removal through the whole upstream subgraph, and it is precisely the way computed is implemented in Quarx.

atom.reportObserved() returns a true if called from within a computation (and hence would be hydrated), and false otherwise.

batch(changes) delays hydration until the changes thunk returns

Basic observables

Using the primitives defined in the previous section one can construct observables of arbitrarily complex behavior. observable.box and computed are two classical basic building blocks of a dependency graph.

  export interface ObservableOptions<T> {
    name?: string;
    equals?: (a: T, b: T) => boolean;
  }

  export namespace observable {
    export function box<T>(value: T, options?: ObservableOptions<T>): Box<T>;
  }

  export function computed<T>(computation: () => T, options?: ObservableOptions<T>): Observable<T>;

Please refer to the API reference for more detail.

Box observables are the upstream leaves of the computations DAG. aBox.get() reports the box observed to the calling computation, and aBox.set(value) will report it changed if the value is different from the current one in the sense of the equals option (=== by default). A Box in Quarx is never trying to make its content deeply observable like MobX. It represents a single observable value.

Computed observables are the intermediate nodes of the DAG representing the reactive derivations. aComputed.get() returns the result of the computation. If the computation threw an error, the computed will store it and re-throw on get(). Only if the computation result is different from the previously computed one in the sense of the equals option (=== by default), the change will be reported downstream.

Computed observables are lazy: if they don't have any observers they will unsubscribe from all their upstream dependencies.

All the observables' and atoms' names are for debug purposes only: they do not affect the execution logic.

Goals and non-goals

The goal for Quarx is to remain a dry essence of a dependency graph engine. As simple and tiny as it is, it will replace MobX in production at ellx.io shortly.

Out of the box, Quarx is not designed to be a state management solution. However, it can be used in combination with Tinyx or even Redux. Just put the root store into a single observable.box, and derive the rest of the state reactively with a network of computed selectors.

On a side note...

Converting an Observable to a Svelte store is a one-liner:

const fromObservable = obs => ({ subscribe: subscriber => autorun(() => subscriber(obs.get())) });
You might also like...

Vanila JS Sparklines library inspired by peity.js

Vanila JS Sparklines library inspired by peity.js

Peity Vanilla JS Converts an element's content into a svg mini pie donut line or bar chart and is compatible with any browser that supports svg: C

Dec 12, 2022

hunter phone script NoPixel_3.0 inspired

hunter phone script NoPixel_3.0 inspired

hunter-phone hunter phone script NoPixel_3.0 inspired And Fixd Lang qb-phone qb-phone mixed between nopixel and iphone Dependencies qb-core qb-policej

Jan 2, 2023

HTML5 Canvas Gauge. Tiny implementation of highly configurable gauge using pure JavaScript and HTML5 canvas. No dependencies. Suitable for IoT devices because of minimum code base.

HTML5 Canvas Gauge. Tiny implementation of highly configurable gauge using pure JavaScript and HTML5 canvas. No dependencies. Suitable for IoT devices because of minimum code base.

HTML Canvas Gauges v2.1 Installation Documentation Add-Ons Special Thanks License This is tiny implementation of highly configurable gauge using pure

Dec 30, 2022

A tiny TypeScript library for 2D vector math.

Vecti A tiny TypeScript library for 2D vector math. Documentation Features ๐Ÿงฎ Addition, subtraction, multiplication and division โœจ Dot, cross and Hada

Nov 30, 2022

vizflow is an ES6 interactive visualization engine

vizflow is an ES6 interactive visualization engine

Vizflow vizflow.js - a render-loop library written using EcmaScript.6 (ES6) with no other external dependencies. Vizflow is a relatively small library

Oct 27, 2022

Flat, round, designer-friendly pseudo-3D engine for canvas & SVG

Zdog Round, flat, designer-friendly pseudo-3D engine View complete documentation and live demos at zzz.dog. Install Download zdog.dist.min.js minified

Jan 3, 2023

Simple HTML5 Charts using the canvas tag

Simple yet flexible JavaScript charting for designers & developers Documentation Currently, there are two versions of the library (2.9.4 and 3.x.x). V

Jan 7, 2023

Simple responsive charts

Simple responsive charts

Big welcome by the Chartist Guy Checkout the documentation site at http://gionkunz.github.io/chartist-js/ Checkout this lightning talk that gives you

Dec 30, 2022

Simple, responsive, modern SVG Charts with zero dependencies

Simple, responsive, modern SVG Charts with zero dependencies

Frappe Charts GitHub-inspired modern, intuitive and responsive charts with zero dependencies Explore Demos ยป Edit at CodePen ยป Contents Installation U

Jan 4, 2023
Comments
  • add memoization support

    add memoization support

    Thanks for the great library!

    It may be nice to use the dependency graph to invalidate memoized computation.

    To memoize an expensive computation users can just replace computed(...) with memoizeOne(...).

    Other solutions like memoize-one require passing in all the inputs as arguments to the function that is memoized.

    opened by philschatz 4
Owner
Dmitry Maevsky
founder at ellx.io
Dmitry Maevsky
A JavaScript library dedicated to graph drawing

sigma.js - v1.2.1 Sigma is a JavaScript library dedicated to graph drawing, mainly developed by @jacomyal and @Yomguithereal. Resources The website pr

Alexis Jacomy 10.3k Jan 3, 2023
a graph visualization library using web workers and jQuery

arbor.js -------- Arbor is a graph visualization library built with web workers and jQuery. Rather than trying to be an all-encompassing framework, a

Christian Swinehart 2.6k Dec 19, 2022
A React toolkit for graph visualization based on G6

Graphin A React toolkit for graph analysis based on G6 English | ็ฎ€ไฝ“ไธญๆ–‡ โœจ Features ?? Good-looking elements, standardized style configuration Graphin st

AntV team 823 Jan 7, 2023
Gephi - The Open Graph Viz Platform

Gephi - The Open Graph Viz Platform Gephi is an award-winning open-source platform for visualizing and manipulating large graphs. It runs on Windows,

Gephi 5.1k Jan 4, 2023
3D graph viewer powered by WebGL (three.js)

Graphosaurus A three-dimensional static graph viewer. (click the image to try it out) Demos EVE Online map Add nodes incrementally Documentation JSDoc

Corey Farwell 362 Dec 4, 2022
A web app that shows visualizations of the most used graphs algorithms such as BFS, DFS, Dijsktra, Minimum spanning tree, etc. It allows you to draw your own graph.

Graph Visualizer Draw your own graphs and visualize the most common graph algorithms This web application allows you to draw a graph from zero, with p

Gonzalo Pereira 31 Jul 29, 2022
Free Bootstrap 5 Admin and Dashboard Template that comes with all essential dashboard components, elements, charts, graph and application pages. Download now for free and use with personal or commercial projects.

PlainAdmin - Free Bootstrap 5 Dashboard Template PlainAdmin is a free and open-source Bootstrap 5 admin and dashboard template that comes with - all e

PlainAdmin 238 Dec 31, 2022
CyberGraph is a 3D-graph based, user based social connection explorer

CyberGraph is a 3D-graph based, user based social connection explorer. It has some cool features like 3d node graph, dynamic loading bar, immersive user experience, cyber mode(10-hops friendship network display) and focus mode(aggregated connection display).

CyberConnect 16 Nov 25, 2022
Babylon.js is a powerful, beautiful, simple, and open game and rendering engine packed into a friendly JavaScript framework.

Babylon.js Getting started? Play directly with the Babylon.js API using our playground. It also contains a lot of samples to learn how to use it. Any

Babylon.js 19.1k Jan 4, 2023