0.0.2 â€Ē Published 2 years ago

fn.obs v0.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

fn.obs

package-badge license-badge size-badge

fn.obs is a tiny (750B 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 (750B minzipped)
  • 🌎 All types are observable (i.e., string, array, object, etc.)
  • ðŸ•ĩïļâ€â™€ïļ Only updates when value has changed
  • ⏱ïļ Batched updates via microtask scheduler
  • 🔎 Deep nested observables via $computed
  • 📞 Simple sub/unsub model via $subscribe
  • 🗑ïļ Mark observables for lazy disposal via $dispose
  • â™ŧïļ Detects cyclic dependencies
  • 🐛 Debugging identifiers
  • 💊 Strongly typed - built with TypeScript

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

Note

Interact with the demo live on StackBlitz.

import { $observable, $computed, $subscribe } from 'fn.obs';

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

// Compute - this will run whenever `$m`, `$x`, or `$b` changes.
const $y = $computed(() => $m() * $x() + $b());

// Subscribe - this will run whenever `$y` is updated.
const unsubscribe = $subscribe(() => console.log($y()));

// `10` will be logged by subscription.
$m.set(10);

// `15` will be logged by subscription.
$b.update((prev) => prev + 5);

// Observable is up-to-date (`15`).
console.log($y());

unsubscribe();

Installation

$: npm i fn.obs

$: pnpm i fn.obs

$: yarn add fn.obs

API

$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 update(). The value can now be observed when used inside other functions such as $computed and $subscribe.

import { $observable } from 'fn.obs';

const $a = $observable(10);

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

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

$computed

Creates a new observable whose value is computed by invoking the given function. Any observables that are read during execution are considered a dependency; they will trigger the compute function to re-run if changed.

import { $observable, $computed } from 'fn.obs';

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

console.log($c()); // logs 20
$a.set(20);
console.log($c()); // logs 30
$b.set(20);
console.log($c()); // logs 40
import { $observable, $computed } from 'fn.obs';

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());

$subscribe

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

import { $observable, $computed, $subscribe } from 'fn.obs';

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

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

// Dispose of the subscription.
unsubscribe();

$peek

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

import { $observable, $computed, $peek } from 'fn.obs';

const $a = $observable(10);

$computed(() => {
  // `$a` changes will not trigger any updates of this computed function.
  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 update()).

import { $observable, $readonly } from 'fn.obs';

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.

import { $observable } from 'fn.obs';

const $a = $observable(10);

$a.set(10);
$a.set(20);
$a.set(30); // only this write is applied
import { $observable, $tick } from 'fn.obs';

const $a = $observable(10);

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

$dispose

Marks the given observable as disposed. Each observable that this observer depends on will delete it on their respective next run (i.e., lazily disposed). This means it will eventually be garbage collected once all observables dependencies are run.

import { $observable, $dispose } from 'fn.obs';

const $a = $observable(10);
$dispose($a);

Debugging

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

import { $observable, $computed } from 'fn.obs';

const $a = $observable(10, 'a');

// Cyclic dependency chain.
const $b = $computed(() => $a() + $c(), 'b');
const $c = $computed(() => $a() + $b(), '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 'fn.obs';

// 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.microtask;

Types

import { $computed, type Observable, type ComputedObservable } from 'fn.obs';

const observable: Observable<number>;
const computed: ComputedObservable<number>;

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

Inspiration

fn.obs was made possible based on my learnings from:

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

0.0.2

2 years ago

0.0.1

2 years ago

0.0.0

2 years ago