npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cplieger/reactive

v1.2.4

Published

Signals + DOM-reconciliation reactive core for TypeScript (zero dependencies)

Readme

reactive

npm JSR Test coverage OpenSSF Best Practices OpenSSF Scorecard

Signals + collections + DOM-reconciliation micro-framework for TypeScript

A standalone reactive app-skeleton: fine-grained signals with automatic dependency tracking and synchronous batched effects, a typed per-key reactive store, a dynamic per-id signal registry (SignalMap), a reactive ordered keyed Collection, a two-tier list render binding (bindList), a CSP-safe element factory (el), keyed-list DOM reconciliation, structural tree-diffing, and a typed event bus (createBus). The store, SignalMap, and Collection are thin facades over the single signal engine — one reactive core, not several. The bus is the deliberate counterpart for discrete events (state lives in signals; events go on the bus). Zero dependencies beyond the DOM API.

Mirrors semantics from @preact/signals-core and solid-js reactivity.

Install

npx jsr add @cplieger/reactive
# or
npm i @cplieger/reactive

Usage

import {
  signal,
  effect,
  batch,
  computed,
  untracked,
  subscribe,
  isSignal,
  isComputed,
} from "@cplieger/reactive";

const count = signal(0);
const doubled = computed(() => count.value * 2);

effect(() => {
  console.log("count:", count.value, "doubled:", doubled.value);
  return undefined;
});

batch(() => {
  count.value = 1;
  count.value = 2; // effect fires once with value 2, synchronously at batch end
});

// Read without tracking
effect(() => {
  const tracked = count.value;
  const notTracked = untracked(() => doubled.value);
  console.log(tracked, notTracked);
  return undefined;
});

// Subscribe utility
const dispose = subscribe(count, (v) => console.log("count changed:", v));
dispose();

// Type guards
isSignal(count); // true
isComputed(doubled); // true

API

Signals

  • signal<T>(initial, options?): Signal<T> — reactive value with .value getter/setter and .peek(). Options: { equals?: false | ((prev, next) => boolean) }
  • computed<T>(fn, options?): ReadonlySignal<T> — lazy cached derived signal (glitch-free, equality-deduped). Options: { equals?: false | ((prev, next) => boolean) }
  • effect(fn: () => Cleanup): () => void — auto-tracking side-effect with cleanup support. fn must return undefined or a cleanup function.
  • batch(fn) — coalesce signal writes; effects flush synchronously at end of outermost batch
  • flushSync() — synchronously drain pending effects (no-op inside batch)
  • untracked<T>(fn): T — run fn without tracking signal reads (like Preact untracked() / Solid untrack())
  • on(deps, fn, options?): () => U — explicit dependency declaration helper (like Solid on()). Returns () => U; with { defer: true } the accessor returns () => U | undefined (the deferred first call yields undefined). A single accessor () => T types value/prev as T; an array of accessors types them as unknown[]. Pass into effect() or computed().
  • subscribe(signal, cb): () => void — subscribe to a signal, calling cb immediately and on every change
  • isSignal(value): boolean — type guard for signals created by signal()
  • isComputed(value): boolean — type guard for computed signals
  • setEffectErrorHandler(handler): EffectErrorHandler — set global error handler for effects; returns the previous handler

DOM building & reconciliation

  • el(tag, attrs?, ...children) — CSP-safe element factory (the build half; reconcile/patch are the commit half). className → class; on* → handler property + trackHandler (so patch reconciles handlers); boolean DOM props (hidden/disabled/checked/selected/…) and value/colSpan/rowSpan/tabIndex/htmlFor → properties; everything else (data-*, aria-*, style, id) → setAttribute. String children become text nodes (never parsed as HTML); null/undefined attrs and children are skipped.
  • reconcile<T>(parent, items, spec) — keyed-list DOM reconciliation with mount/update/onRemove lifecycle
  • patch(parent, ...children) — structural tree-diff, replacing a parent's children with reconciled new nodes. Element nodes are keyed by their first data-col or *-id attribute (so reorders/re-patches reuse the matched node); unkeyed nodes match by position.
  • reconcileChildren(parent, newChildren) — low-level child reconciliation against existing DOM
  • trackHandler(el, key) — register an on* property for handler reconciliation during tree-diff

Store

createStore and SignalMap are thin facades over the one signal engine — there is no second reactivity implementation. createStore lazily backs each key with a signal; SignalMap is a registry of signals keyed by a runtime string id.

  • createStore<M>(): Store<M> — typed, fixed-key reactive store with get, set, subscribe, effect, computed, and batch. subscribe notifies on change only (not immediately on subscribe). A computed key whose fn reads its own output does not loop unbounded: a self-read that keeps yielding new values trips the engine's batch-iteration guard and surfaces Error("Cycle detected") through the effect error handler (effects isolate errors, so it is not rethrown to the caller), while a stable self-read settles via Object.is dedup.
  • SignalMap<V> — dynamic per-id signal registry: get(id), ensure(id, initial), clear(id), clearAll(). For reactive state whose key set isn't known at the type level (per-message streaming text, per-row state, …); complements createStore's fixed key set.

Collections

A reactive ordered collection of keyed entities — the data half of the two-tier list pattern. A per-entity content update touches only that entity's subscribers; add/remove/reorder bumps the structure signal (ids). Built on signal + SignalMap.

  • createCollection<T>(keyOf: (item: T) => string): Collection<T> — returns a collection with:
    • setAll(items) — replace everything (same-order replacement does not bump ids)
    • upsert(item) — add/replace one (appends new ids); prepend(items) — add to the front (scroll-up/load-older pagination)
    • update(id, next | updater) / remove(id) / clear()
    • get(id) / has(id) (untracked), signalFor(id) (reactive per-entity), size
    • ids: ReadonlySignal<readonly string[]> — structure tier (add/remove/reorder only)
    • items() — ordered reactive snapshot (tracks order + every entity)

List rendering

  • bindList<T>(parent, source, spec): () => void — two-tier render binding. source is a ListSource<T> = { ids, signalFor }; a Collection satisfies it directly, and a filtered/sorted/paginated view is just { ids: computed(...), signalFor: collection.signalFor } (pagination = a sliced view, not a separate primitive). One structural effect tracks source.ids and reconciles the row list; each row owns a private effect tracking only its own entity signal, so a per-entity change repaints just that row with no structural reconcile. spec: { mount(item, id) => HTMLElement; update?(el, item, id); onRemove?(el, id) } (update runs at mount and on every later change). Returns a dispose that tears down the structural effect and every row effect.

Event bus

State lives in signals; discrete events go on a bus. createBus is the typed event primitive (not reactive state — no retained value).

  • createBus<EventMap>(options?): Bus<EventMap>on/once/off/emit/clear. Handlers are snapshot-cached (rebuilt only on mutation; a handler unsubscribed mid-emit still fires for that emit). Events whose payload type is undefined emit with no payload argument. A throwing handler is isolated via options.onError (default console.error).

Correctness guarantees

  • Glitch-freedom: Computed signals use equality dedup (Object.is) — downstream effects only fire when the computed value actually changes. Diamond dependency graphs are handled correctly.
  • Cycle detection: Reading a computed that is currently being computed throws Error("Cycle detected").
  • Error caching: If a computed's fn throws, the error is cached and rethrown on subsequent reads until dependencies change.
  • Computed is read-only: Setting .value on a computed throws Error("Cannot set a computed signal").
  • Synchronous batch: batch() flushes effects synchronously at the end of the outermost batch (matching @preact/signals-core and solid-js).

Design Decisions — Unsupported by Design

The following features are intentionally NOT implemented:

| Feature | Reason | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Effect ownership tree / createRoot | Consumers manage disposal explicitly. Adding ownership changes the mental model. | | Nested effect auto-disposal | Each effect is independent and returns its own dispose function. Compose with arrays or helper functions. | | Lazy activation / onMount lifecycle | Signals are always active. Resource management is the consumer's responsibility. | | Signal.subtle.Watcher / notify-on-dirty | This library IS the framework layer. The Watcher pattern is for frameworks that sit on top of a signals primitive. | | Introspection APIs | Dev-tools concern. Not needed for production. | | Explicit computed disposal | Computed signals are GC'd when unreferenced. No explicit teardown needed. | | SSR / server-side isolation | Client-side library. Server usage should instantiate fresh signal graphs per request. | | Async signals / resources | Out of scope. Use effects + manual signal writes for async data loading. | | Transactions | Framework-level concern. Not a signals primitive. | | Custom scheduler / setScheduler() | Batch is synchronous. No scheduler needed. |

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0 — see LICENSE.