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

@webreflection/signals

v0.2.8

Published

A minimalistic Preact-like signals implementation

Readme

@webreflection/signals

Social Media Photo by Carlos Alberto Gómez Iñiguez on Unsplash

Coverage Status

A minimalistic Preact-like signals implementation.

Once minified and compressed, this module is actually 0.5KB.

core

// basic core features
import {
  Signal,     // class for brand check
  Computed,   // extends Signal: brand check
  batch,      // Preact-like API
  computed,   // Preact-like API
  effect,     // Preact-like API
  signal,     // Preact-like API
  untracked,  // Preact-like API
} from '@webreflection/signals';

disposable

Exposes a Preact-like createModel utility with a disposable export.

// extra core features
import {
  // extra:
  disposable, // equivalent of createModel(fn)
  // all other exports from core
  ...core
} from '@webreflection/signals/disposable';

In Depth

  • simply (swapped) stack-based, maybe not the best approach, but one that can guarantee reasonable performance with minimal code size.
  • only signal and computed subscribe while reading values, unless sig_or_comp.peek() is used.
  • any effect updates synchronously but then runs only in isolation. Every effect is disposed of if the outer effect is running, meaning stacked effects work out of the box and always™ do the right thing.
  • disposable uses the very same effect logic to dispose itself when not needed anymore.
  • batch piles up subscribers and filters at the end for those that didn't get trashed in between (not perfect, yet fast).
  • untracked temporarily disables subscription in both read (for symbol and computed) and write (for symbol only).

Background

You know, nowadays it's hard to find libraries that are still 100% under control, minimalistic, not bloated, yet correct, and this one would like to be one of those 😇

Benchmark

benchmark

Architecture

Fine-tuned signals are a piece of art:

  • fastest possible feedback
  • linked graphs with bitwise flags deciding what to do and/or when (alien-signals)
  • transpiled and understood ahead of time to produce the best possible outcome (Svelte, SolidJS)
  • ad-hoc or coupled DOM manipulation
  • specific to JSX syntax (Preact or alternatives)
  • ... other attempts/variants out there

That is all good and fine, yet the graph behind signals is, imho, pretty simple in theory (clearly hard in practice) ... and I will tell you how this module keeps that simple concept in mind.

These are just a value wrapper: you reach that value? You are subscribing to it. You change that value? You are triggering anything listening to that signal reference after subscribing.

That's it, that's the contract!

Here, there is a .peek() method to avoid subscribing, but any time you access a signal.value, you are subscribing to it if you are either a computed reference or an event one.

It's a signal by all means, because once you reach its value, it subscribes to any subscriber, just like any signal would do.

The main difference between computed and signal is that computed is a read-only contract, and it expects a callback as an argument that will "brand" that computed type from then on.

Everything else is the same: you cannot computed.value = anything but you can always retrieve computed.value to subscribe to that computed.

When a dependency changes, a computed is not immediately executed again: it is simply marked as invalid. That invalid state is also a guard, because once a computed is already known to be stale there is no reason to notify its subscribers again for every other signal change happening in the same flow, or while that computed is already resolving itself. The next .value or .peek() access refreshes it once, producing the latest result from the current state.

This is the whole orchestration around signals or computed that makes anything reactive, but because it's a bottom-up situation we're dealing with, things might feel overly complicated. In theory, that's not the case.

const num = signal(0);

const dispose = effect(() => {
  // subscribe to this signal state
  const value = num.value;

  // make this effect able to dispose itself
  if (2 <= value) {
    // no further changes to num will be observed
    dispose();
  }
  else {
    console.log({ value });
  }
});

// logs: { value: 0 }

// increment by 1
num.value++;
// logs: { value: 1 }

// increment by 1
num.value++;
// logs nothing!

// drop reactivity explicitly!
dispose();

// increment by 1
num.value++;
// also logs nothing!

In this example, the effect subscribes to num changes, but it seppukus itself once its value reaches the number 2 or above ("how is that possible?" ... you'll learn that in a bit!).

The architecture in this example is also easy to explain: any signal or computed value that is reached will consider its outer effect a potential subscriber!

The important difference in this module is that effect has no notion of what it subscribed to. It's the signal or computed that retains that information, not the consumer, and that's simply because invoking any foreign callback doesn't mean you know what happens within that callback. Not knowing what happens is indeed a great way to understand this architecture.

Long story short, any callback executing within an effect is registered as a consumer of the current signal or computed reference that is required to provide a .value while the callback is happening, so the relation is from signal or computed to any running callback, if any.

Effect in details

No effect? No reactivity! This is the signals contract, but there is a catch:

  • what if an effect is within another effect?
  • how can conditional operators ditch what sub-effect should run and what shouldn't?

Great questions. Here are the details about why that's never a concern:

  • effect never add subscribers to itself, like signals or computeds do, it just registers itself as an observer (subscriber)
  • effect never runs if it knows outer effects are queued to resolve the latest change or changes are happening while it's running
  • the previous point means if signal.value is registered both at the inner effect level and at the outer one, the outer one will dictate the execution because ...
  • only the top-most subscribed effects will eventually execute, and ...
  • any effect previously registered for its outer effect will be disposed and never react to anything again!

I am not sure you are still following, but because effect is a bottom-up problem, top-down is clearly the solution, and that's granted by the registration stack, where the outer effect runs before the inner effect. That solves everything!

The same invalid guard used by computed values applies to effects too: when multiple signals ask the same effect to refresh, only the first invalidation matters until that effect has actually run again. This avoids repeated work, avoids repeated notifications, and prevents an effect from recursively refreshing itself while its own callback is still on the stack.

If you followed everything else I've explained around this architecture, batch(callback) simply represents a running callback with no instant reactivity, it simply accumulates changes and trigger after all changes happend for whatever effect was involved.

This utility basically runs updates and whatnot like batch, but it will never register itself while doing that execution, resulting in a safe hook for foreign functions that wouldn't otherwise belong to our logic. They are just interested in our data instead, and that's fine!

Differences from Preact Signals

This module aims to be Preact-like, not a byte-for-byte or behavior-for-behavior clone of Preact Signals. The goal is to keep the core small, predictable, and easy to reason about, while still exposing Signal and Computed classes for projects that want to specialize any behavior.

The following differences are intentional tradeoffs:

  • assigning the same value still notifies subscribers. If you want distinct-value semantics, extend Signal and provide your own factory:
import { Signal as WRSignal } from '@webreflection/signals';

class Signal extends WRSignal {
  get value() {
    return super.value;
  }

  set value(value) {
    if (!Object.is(this.peek(), value)) {
      super.value = value;
    }
  }
}

export const signal = value => new Signal(value);
  • conditional dependencies are not pruned after every run. Instead, subscribers are kept by the signal or computed reference and are pruned lazily when that source notifies again, skipping disposed effects along the way. This might look "leaky" at first glance, but every Signal and Computed implements native Symbol.dispose, so the latest JavaScript using syntax can deterministically clear retained subscribers when a scope is done:
{
  using count = signal(0);
  using doubled = computed(() => count.value * 2);

  effect(() => console.log(doubled.value));
}
// count and doubled are disposed here

If a source is no longer referenced, normal garbage collection takes care of it; if it is still referenced, the next notification keeps the final value valid and fast enough to retrieve without maintaining a more complex dependency graph.

  • mixed direct signal reads and derived computed reads can observe an intermediate stale computed value in the same effect, because there is no graph ordering involved. For example, reading both count.value and doubled.value where doubled is computed from count can briefly see the updated count with the previous doubled before the computed invalidation catches up. This is the little extra cost paid by a not-so-common scenario to keep the library as small as it is.

  • batch(callback) coalesces execution, but it does not perform Preact's final-value reconciliation. This follows from the first point: signals do not compare old and new values, so signal.value = signal.value is still a notification. That can be a desirable pattern in some cases; for every other "don't do that" case, extend Signal as shown above.

  • effect(callback) callbacks must return either void or a cleanup function, as specified by the TypeScript contract. Returning any other value is unsupported and will throw later if that value is invoked as cleanup.

  • extra Preact APIs such as subscribe, valueOf, toJSON, and action are outside the minimal core. You can extend the exported classes to add the hooks that make sense for your project. Preact's createModel API surface is covered as much as this module needs by the explicit @webreflection/signals/disposable entry point and its disposable exported utility, so model-style disposal stays available when imported on purpose without being embedded into the default signals primitive.

Please Note

Because the public shape intentionally follows the Preact Signals API, this module can also be replaced by the real Preact Signals package whenever a project needs its full graph, helpers, or framework integrations. In that sense, it is meant to make the API familiar without forcing anyone to keep using this smaller implementation, or its intentional constraints, forever.