fn.obs v0.0.2
fn.obs
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 ð