/ parabun

bun:signals

Reactive cells with auto-derived computations and microtask-flushed effects.

ts
import { signal, derived, effect, batch, untrack, Signal } from "bun:signals";

bun:signals is a small reactive primitive. signal(v) is a cell, derived(fn) is a read-only signal computed from others, and effect(fn) runs side effects when something it read changes. Reads inside an effect register a dependency; writes invalidate downstream and a microtask flush re-runs only the effects whose observed values actually changed.

Pairs with the signal / effect { } / ~> language extensions — those desugar to calls into this module. Plain .ts / .js files use the function form below.

signal(initial)

Creates a writable cell.

ts
import { signal } from "bun:signals";

const count = signal(0);

count();          // read → 0
count.get();      // read → 0
count(1);         // write
count.set(2);     // write
count.update(n => n + 1);   // read-modify-write

count is callable: with no args it reads, with one arg it writes. The explicit .get() / .set() methods are also there for clarity. update(fn) is set(fn(get())) in one atomic-ish step.

derived(fn)

A read-only signal computed from others. Tracks every signal fn reads; re-evaluates when any of them changes.

ts
const double = derived(() => count() * 2);
double();         // 4 (after count(2))

derived is lazy — it only re-evaluates when read after an invalidation. Multiple reads between writes return the cached value.

effect(fn)

Runs fn immediately, tracks its signal reads, and re-runs whenever any of them changes. Returns a disposer that removes the effect.

ts
const dispose = effect(() => {
  console.log("count is", count());
});

count(3);          // logs "count is 3" on next microtask
dispose();          // stop watching

Effects fire on a microtask after the write, so multiple writes within the same synchronous code path coalesce into one re-run.

batch(fn)

Defers effect re-runs until fn returns:

ts
batch(() => {
  count(1);
  name("alice");
  // no effects re-run yet
});
// effects re-run once with both new values visible

untrack(fn)

Reads inside fn don't register as dependencies — useful inside an effect to read a signal "for context" without making the effect re-run when it changes.

ts
effect(() => {
  console.log(count(), "at", untrack(() => Date.now()));
  // re-runs on count change, NOT on every Date.now read
});

Signal<T> type

Exported for type annotations. signal(0) returns a Signal<number>; derived(...) returns a Signal<T> (read-only — TypeScript marks .set / .update as never).

Composing with the rest of the stack

Limits