A lightweight reactivity API for other UI libraries to be built on top of.

Overview

Observables

package-badge license-badge size-badge

🏆 The goal of this library is to provide a lightweight reactivity API for other UI libraries to be built on top of. It follows the "lazy principle" that Svelte adheres to - don't do any unnecessary work and don't place the burden of figuring it out on the developer.

This is a tiny (~850B minzipped) library for creating reactive observables via functions. You can use observables to store state, create computed properties (y = mx + b), and subscribe to updates as its value changes.

  • 🪶 Light (~850B minzipped)
  • 💽 Works in both browsers and Node.js
  • 🌎 All types are observable (i.e., string, array, object, etc.)
  • 🕵️‍♀️ Only updates when value has changed
  • ⏱️ Batched updates via microtask scheduler
  • 😴 Lazy by default - efficiently re-computes only what's needed
  • 🔬 Computations via $computed
  • 📞 Effect subscriptions via $effect
  • ♻️ Detects cyclic dependencies
  • 🐛 Debugging identifiers
  • 💪 Strongly typed - built with TypeScript

⏭️ Skip to API

⏭️ Skip to TypeScript

⏭️ Skip to Benchmarks

Here's a simple demo to see how it works:

Open in StackBlitz

import { $root, $observable, $computed, $effect, $tick } from '@maverick-js/observables';

$root(async (dispose) => {
  // Create - all types supported (string, array, object, etc.)
  const $m = $observable(1);
  const $x = $observable(1);
  const $b = $observable(0);

  // Compute - only re-computed when `$m`, `$x`, or `$b` changes.
  const $y = $computed(() => $m() * $x() + $b());

  // Effect - this will run whenever `$y` is updated.
  const stop = $effect(() => {
    console.log($y());

    // Called each time `$effect` ends and when finally disposed.
    return () => {};
  });

  $m.set(10); // logs `10` inside effect

  // Wait a tick so update is applied and effect is run.
  await $tick();

  $b.next((prev) => prev + 5); // logs `15` inside effect

  // Wait a tick so effect runs last update.
  await $tick();

  // Nothing has changed - no re-compute.
  $y();

  // Stop running effect.
  stop();

  // ...

  // Dispose of all observables inside `$root`.
  dispose();
});

Export Sizes

Library export sizes

Average: ~600B. Total: ~850B.

You can also check out the library size on Bundlephobia (less accurate).

Installation

$: npm i @maverick-js/observables

$: pnpm i @maverick-js/observables

$: yarn add @maverick-js/observables

API

$root

Computations are generally child computations. When their respective parent is destroyed so are they. You can create orphan computations (i.e., no parent). Orphans will live in memory until their internal object references are garbage collected (GC) (i.e., dropped from memory):

import { $computed } from '@maverick-js/observables';

const obj = {};

// This is an orphan - GC'd when `obj` is.
const $b = $computed(() => obj);

Orphans can make it hard to determine when a computation is disposed so you'll generally want to ensure you only create child computations. The $root function stores all inner computations as a child and provides a function to easily dispose of them all:

import { $root, $observable, $computed, $effect } from '@maverick-js/observables';

$root((dispose) => {
  const $a = $observable(10);
  const $b = $computed(() => $a());

  $effect(() => console.log($b()));

  // Disposes of `$a`, $b`, and `$effect`.
  dispose();
});
// `$root` returns the result of the given function.
const result = $root(() => 10);

console.log(result); // logs `10`

$observable

Wraps the given value into an observable function. The observable function will return the current value when invoked fn(), and provide a simple write API via set() and next(). The value can now be observed when used inside other computations created with $computed and $effect.

import { $observable } from '@maverick-js/observables';

const $a = $observable(10);

$a(); // read
$a.set(20); // write (1)
$a.next((prev) => prev + 10); // write (2)

Warning Read the $tick section below to understand batched updates.

$computed

Creates a new observable whose value is computed and returned by the given function. The given compute function is only re-run when one of it's dependencies are updated. Dependencies are are all observables that are read during execution.

import { $observable, $computed, $tick } from '@maverick-js/observables';

const $a = $observable(10);
const $b = $observable(10);
const $c = $computed(() => $a() + $b());

console.log($c()); // logs 20

$a.set(20);
await $tick();
console.log($c()); // logs 30

$b.set(20);
await $tick();
console.log($c()); // logs 40

// Nothing changed - no re-compute.
console.log($c()); // logs 40
import { $observable, $computed } from '@maverick-js/observables';

const $a = $observable(10);
const $b = $observable(10);
const $c = $computed(() => $a() + $b());

// Computed observables can be deeply nested.
const $d = $computed(() => $a() + $b() + $c());
const $e = $computed(() => $d());

$effect

Invokes the given function each time any of the observables that are read inside are updated (i.e., their value changes). The effect is immediately invoked on initialization.

import { $observable, $computed, $effect } from '@maverick-js/observables';

const $a = $observable(10);
const $b = $observable(20);
const $c = $computed(() => $a() + $b());

// This effect will run each time `$a` or `$b` is updated.
const stop = $effect(() => console.log($c()));

// Stop observing.
stop();

You can optionally return a function from inside the $effect that will be run each time the effect re-runs and when it's finally stopped/disposed of:

$effect(() => {
  return () => {
    // Called each time effect re-runs and when disposed of.
  };
});

$peek

Returns the current value stored inside an observable without triggering a dependency.

import { $observable, $computed, $peek } from '@maverick-js/observables';

const $a = $observable(10);

$computed(() => {
  // `$a` will not be considered a dependency.
  const value = $peek($a);
});

$readonly

Takes in the given observable and makes it read only by removing access to write operations (i.e., set() and next()).

import { $observable, $readonly } from '@maverick-js/observables';

const $a = $observable(10);
const $b = $readonly($a);

console.log($b()); // logs 10

// We can still update value through `$a`.
$a.set(20);

console.log($b()); // logs 20

$tick

Tasks are batched onto the microtask queue. This means only the last write of multiple write actions performed in the same execution window is applied. You can wait for the microtask queue to be flushed before writing a new value so it takes effect.

Note You can read more about microtasks on MDN.

import { $observable } from '@maverick-js/observables';

const $a = $observable(10);

$a.set(10);
$a.set(20);
$a.set(30); // only this write is applied
import { $observable, $tick } from '@maverick-js/observables';

const $a = $observable(10);

// All writes are applied.
$a.set(10);
await $tick();
$a.set(20);
await $tick();
$a.set(30);

$dispose

Unsubscribes the given observable and optionally all inner computations. Disposed functions will retain their current value but are no longer reactive.

import { $observable, $dispose } from '@maverick-js/observables';

const $a = $observable(10);
const $b = $computed(() => $a());

// `$b` will no longer update if `$a` is updated.
$dispose($a);

$a.set(100);
console.log($b()); // still logs `10`

onDispose

Runs the given function when the parent computation is disposed of:

import { $effect, onDispose } from '@maverick-js/observables';

const listen = (type, callback) => {
  window.addEventListener(type, callback);
  // Called when the effect is re-run or finally disposed.
  onDispose(() => window.removeEventListener(type, callback));
};

const stop = $effect(
  listen('click', () => {
    // ...
  }),
);

stop(); // `onDispose` is called

The onDispose callback will return a function to clear the disposal early if it's no longer required:

$effect(() => {
  const dispose = onDispose(() => {});
  // ...
  // Call early if it's no longer required.
  dispose();
});

isObservable

Whether the given value is an observable (readonly).

// True
isObservable($observable(10));
isObservable($computed(() => 10));
isObservable($readonly($observable(10)));

// False
isObservable(false);
isObservable(null);
isObservable(undefined);
isObservable(() => {});

isSubject

Whether the given value is an observable subject (i.e., can produce new values via write API).

// True
isSubject($observable(10));

// False
isSubject(false);
isSubject(null);
isSubject(undefined);
isSubject(() => {});
isSubject($computed(() => 10));
isSubject($readonly($observable(10)));

Debugging

The $observable, $computed, and $effect functions accept a debugging ID (string) as part of their options. This can be helpful when logging a cyclic dependency chain to understand where it's occurring.

import { $observable, $computed } from '@maverick-js/observables';

const $a = $observable(10, { id: 'a' });

// Cyclic dependency chain.
const $b = $computed(() => $a() + $c(), { id: 'b' });
const $c = $computed(() => $a() + $b(), { id: 'c' });

// This will throw an error in the form:
// $: Error: cyclic dependency detected
// $: a -> b -> c -> b

Note This feature is only available in a development or testing Node environment (i.e., NODE_ENV).

Scheduler

We provide the underlying microtask scheduler incase you'd like to use it:

import { createScheduler } from '@maverick-js/observables';

// Creates a scheduler which batches tasks and runs them in the microtask queue.
const scheduler = createScheduler();

// Queue tasks.
scheduler.enqueue(() => {});
scheduler.enqueue(() => {});

// Schedule a flush - can be invoked more than once.
scheduler.flush();

// Wait for flush to complete.
await scheduler.tick;

Note You can read more about microtasks on MDN.

TypeScript

import {
  isObservable,
  isSubject,
  type Effect,
  type Observable,
  type MaybeObservable,
} from '@maverick-js/observables';

// Types
const observable: Observable<number>;
const computed: Observable<string>;
const effect: Effect;

// Provide generic if TS fails to infer correct type.
const $a = $computed<string>(() => /* ... */);

const $b: MaybeObservable<number>;

if (isObservable($b)) {
  $b(); // Observable
}

if (isSubject($b)) {
  $b.set(10); // ObservableSubject
}

Benchmarks

Layers

This benchmark was taken from cellx. It tests how long it takes for an n deeply layered computation to update. The benchmark can be found here.

Each column represents how deep computations were layered. The average time taken to update the computation out of a 100 runs is used for each library.

Don't take this benchmark too seriously because it hasn't been reviewed properly at all.

Layers benchmark

Inspiration

@maverick-js/observables was made possible based on my learnings from:

Special thanks to Wesley, Julien, and Solid/Svelte contributors for all their work 🎉

Comments
  • No reaction?

    No reaction?

    Don't feel obligated to respond to this at all. 😄

    But I had this thing based on another reactive library:

    https://codesandbox.io/s/reactive-dom-lifecycle-zhzw7

    A basic, working Sinuous clone using dipole.

    Well, your library looks awesome, so of course I had to try to port from dipole to that:

    https://codesandbox.io/s/reactive-dom-maverick-njvhsw

    Essentially, there's no effects happening anywhere, and I have no clue why. 🤷‍♂️

    As said, don't feel obligated to respond - this is most likely not due to any issue with your library, but just my lacking understanding of how it works. I would have posted this in Discussions if it were enabled. 😄

    opened by mindplay-dk 14
  • tick() causes error

    tick() causes error

    import { signal, effect, tick } from '@maverick-js/signals'
    
    let res = []
    const A = signal(0)
    const H = effect( ()=> res.push( A() ) )
    
    A.set(1); tick()
    
    $mol_assert_like( res, [ 1 ] )
    

    Sandbox

    opened by nin-jin 2
  • bench: S.js tests creating 2 computations per node

    bench: S.js tests creating 2 computations per node

    Hi, I'm the author of S.js.. I learned about your framework and its benchmark when it was mentioned in a twitter conversation: https://twitter.com/RyanCarniato/status/1574305237933821953 .

    I noticed that your benchmark for S.js has it doing 2x the work of the other implementations, as it's creating a second, unnecessary computation for each node. Specifically, this line is redundant and should be removed.

            S(props.a), S(props.b), S(props.c), S(props.d);
    

    props.a/b/c/d are already computations. Wrapping them a second time with S(...) makes a second, unnecessary computation out of those computations.

    I don't know the history of this benchmark and gather that you may have copied it from somewhere else. If the upstream source has the same issue, would you mind pointing me towards it, so that I can get it fixed there too? Thanks!

    opened by adamhaile 2
  • Small bug in this fallback

    Small bug in this fallback

    This line:

    https://github.com/maverick-js/observables/blob/522c526fd63c0c8a1a6e47f410e754bd6ee60213/src/scheduler.ts#L25

    This fallback won't work - if queueMicrotask not defined, it will fail.

    You need something like window.queueMicrotask, or more likely something like typeof queueMicrotask != 'undefined' to remain compatible with Node.

    Very nice implementation of observables! Very small and very readable codebase. 🙂🙏

    opened by mindplay-dk 2
  • benchmark not cheking corretness

    benchmark not cheking corretness

    The current benchmark is using .every to just return truthy values, instead of really checking correct results.

    This MR fixes that, otherwise the result will e true no matter what the library computed.

    opened by WebReflection 1
  • What is this.

    What is this.

    This is the freshly installed clone, running pnpm test.

     ❯ tests/observables.test.ts (0)
    
    ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
    
     FAIL  tests/observables.test.ts [ tests/observables.test.ts ]
    SyntaxError: Unexpected token '??='
     ❯ new Script vm.js:102:7
     ❯ createScript vm.js:262:10
     ❯ Object.runInThisContext vm.js:310:10
     ❯ async /home/eguneys/js/observables/src/index.ts:1:256
    
    ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
    
    Test Files  1 failed (1)
         Tests  no tests
          Time  2.87s (in thread 0ms, Infinity%)
    
     ELIFECYCLE  Test failed. See above for more details.
    
    opened by eguneys 1
  • Array reconciliation like `mapArray` in Solid.js

    Array reconciliation like `mapArray` in Solid.js

    Like this:

    let $a = $observable([1,2,3])
    
    let $b = $computed($mapArray($a, _ => { console.log(_); return { _ }))
    
    // logs 1 2 3
    
    $a.set([3,2,5])
    
    // logs 5
    

    Finally I want to know 1 is removed so some type of $onCleanup function inside $computed.

    Also some type of notification to note that elements have reordered.

    opened by eguneys 1
  • Benchmarks: Fix sinuous benchmarks and don't let cellx eat up all of our precious memory

    Benchmarks: Fix sinuous benchmarks and don't let cellx eat up all of our precious memory

    This pull request modifies the layers benchmarks in two ways:

    • The sinuous benchmark now works, the imports needed to be a bit different.

    • Add a small setTimeout(..., 0) breather between different benchmarks. On my machine running the benchmark crashed due to Node.js running out of memory. Turns out that cellx uses a global pendingCells array that's filled during processing and is released asynchronously in batches. Especially in the last 2500 tier the amount of memory retained by cellx tended to become over 4 gigabytes. It also made things a bit unfair for libraries benchmarked after cellx, as they had to deal with the bloated heap. The timeout lets cellx return the memory in time and the GC do its magic.

    • Update: After submitting a similar change to https://github.com/elbywan/hyperactiv I noticed that the cellx benchmark can be made to interfere with the other libraries' benchmarks even less by manually disposing the root cells after each cellx run. See also https://github.com/elbywan/hyperactiv/pull/28

    Thank you for a great benchmark, it has been useful for gauging differences between approaches 🙂

    opened by jviide 0
  • Get previous values of observables inside effect via `on` as in Solid.js

    Get previous values of observables inside effect via `on` as in Solid.js

    for example this:

    let $a = $observable(3)
    $effect($on($a, (prev, value) => {
      console.log(prev, value)
    }))
    
    // first, logs: undefined, 3
    $a.set(5)
    // now logs: 3, 5
    
    opened by eguneys 0
Owner
Maverick
JS-F18
Maverick
basic implementation of the Vue 3 reactivity engine - from scratch

Vue 3 Reactivity This material was created by Marc Backes in order to show how reactivity is solved in Vue 3. It contains a basic implementation of th

Marc Backes 13 Oct 6, 2022
A tiny ( 1KB) reactivity library

influer A tiny (< 1KB) reactivity library yarn add influer Introduction influer is a tiny reactivity library. Exported in ESM, CJS, and IIFE, all < 1K

Tom Lienard 7 Jul 13, 2022
A reactive filesystem interface based on Vue 3 reactivity system.

A reactive filesystem interface based on Vue 3 reactivity system.

Guillaume Chau 37 Oct 8, 2022
Toy Level Reactivity like Vue 3.x

reactivity Reactivity like Vue 3.x Features Typescript JSX Support Components and Fragment Dependency-Collect Reactivity Reactive Object based on ES6

Hydrogen 4 Oct 17, 2022
Compile-time reactivity for JS

silmaril Compile-time reactivity for JS Install npm install --save silmaril yarn add silmaril pnpm add silmaril Features Compile-time reactivity Minim

Alexis H. Munsayac 27 Nov 10, 2022
A lightweight library to create reactive objects

Reactivity As the name says, Reactivity is a lightweight javascript library to create simple reactive objects. Inspired in Redux and Vuex Get started

David Linarez 2 Oct 28, 2021
Lightweight react-like library. Support for asynchronous rendering and hooks.

Recept · Lightweight react-like library. Like the name, this project is mainly based on the architectural idea of react, which can feel react more int

RuiLin Dong 52 Sep 17, 2022
🤯 zART-stack — Zero-API, React [Native], & TypeScript

?? zART-stack — Zero-API, React [Native], & TypeScript

Alex Johansson 674 Jan 3, 2023
Maple.js is a React webcomponents based framework mixing ES6 with Custom Elements, HTML Imports and Shadow DOM. It has in-built support for SASS and JSX, including a Gulp task for vulcanizing your project.

Heroku: http://maple-app.herokuapp.com/ npm: npm install maple.js Bower: bower install maple.js Maple is a seamless module that allows you to organise

Adam Timberlake 430 Dec 23, 2022
Magic Quadrant built with React & Typescript

Magic Quadrant Interactive Magic Quadrant built with React & Typescript Demo Demo Link Firebase: https://magic-quadrant-3eaf1.web.app/ Usage Install D

Aykut Ulış 7 Apr 10, 2022
Minimalist and lightweight UI library, even with its own markup language and a CSS-In-JS library built into the template, Router and a powerful template reactivity system.

Translations: Português do Brasil Bemtv(Abbreviation of the bird's name Bem-te-vi) is a JavaScript library that brings a new approach to creating inte

Diogo Neves 36 Dec 19, 2022
nivo provides a rich set of dataviz components, built on top of the awesome d3 and Reactjs libraries

nivo provides supercharged React components to easily build dataviz apps, it's built on top of d3. Several libraries already exist for React d3 integr

Raphaël Benitte 10.9k Jan 7, 2023
Add Prometheus metrics to your React App. Built on top of promjs and react-performance libraries

prom-react Add Prometheus metrics to your React App. Built on top of promjs and react-performance libraries Scope and purpose The main objective of th

Cabify 16 Dec 2, 2022
A toolkit for React, Preact, Inferno & vanilla JS apps, React libraries and other npm modules for the web, with no configuration (until you need it)

nwb nwb is a toolkit for: Quick Development with React, Inferno, Preact or vanilla JavaScript Developing: React Apps Preact Apps Inferno Apps Vanilla

Jonny Buchanan 5.5k Jan 3, 2023
A toolkit for React, Preact, Inferno & vanilla JS apps, React libraries and other npm modules for the web, with no configuration (until you need it)

nwb nwb is a toolkit for: Quick Development with React, Inferno, Preact or vanilla JavaScript Developing: React Apps Preact Apps Inferno Apps Vanilla

Jonny Buchanan 5.5k Dec 21, 2022
Resturant Management System backend was provided by Client so we did front end using ANT Design, Bootstrap and other Libraries of ReactJs

Resturant Management System backend was provided by Client so we did front end using ANT Design, Bootstrap and other Libraries of ReactJs

Safiullah 20 Sep 17, 2022
React Native Typescript Template with scalable design and cutting edge technologies like CodePush, Sentry and other libraries pre-configured to save your time.

A React Native Template/Boilerplate containing the best practices and scalable design with cutting edge technologies like CodePush/Sentry and all other neccessary libraries pre-configured and basic helper functions and components to help you save your time and make your App super fast.

KASHAN HAIDER 23 Dec 1, 2022
Immutable data structures with history for top-to-bottom properties in component based libraries like React. Based on Immutable.js

Immstruct A wrapper for Immutable.js to easily create cursors that notify when they are updated. Handy for use with immutable pure components for view

null 376 Nov 10, 2022
My personal portfolio where you can find my top projects and other details. I have used react to develop this website.

Portfolio website Reactjs Browse My React js responsive Portfolio source code above. You can easily clone this Portfolio website with Suraj Sahu's cod

Suraj Sahu 5 Oct 18, 2022
Lightweight React library for drawing network graphs built on top of SigmaJS

It makes easy to publish networks on Web pages and allows developers to integrate network exploration in rich Web applications. Use JSX for graph conf

Maxim Vorobjov 246 Dec 20, 2022