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

@web-loom/signals-core

v0.6.0

Published

Framework-agnostic reactive signals with computed values and effects.

Downloads

60

Readme

@web-loom/signals-core

Framework-agnostic reactive signals with computed values and effects.

Installation

npm install @web-loom/signals-core

Quick start

import { signal, computed, effect, batch } from '@web-loom/signals-core';

// Writable signal
const count = signal(0);

// Derived computed (lazy, memoized)
const doubled = computed(() => count.get() * 2);

// Side effect — runs immediately, reruns when dependencies change
const handle = effect(() => {
  console.log('count:', count.get(), 'doubled:', doubled.get());
  return () => console.log('cleanup');
});

count.set(5); // logs: cleanup  count: 5  doubled: 10

// Batch multiple updates into a single notification flush
batch(() => {
  count.set(10);
  count.set(20);
}); // effect runs once with final value

// Stop the effect
handle.dispose();

API

signal<T>(initial, options?)

Creates a writable reactive value.

const name = signal('Alice');

name.get();              // read — tracked inside computed/effect
name.peek();             // read without tracking (no dependency registered)
name.set('Bob');         // write — notifies subscribers if value changed
name.update(v => v + '!'); // update based on previous value
name.asReadonly();       // returns a ReadonlySignal view (hides set/update)
name.subscribe(fn);      // low-level subscription — returns unsubscribe fn

Options:

signal(value, {
  equals: (a, b) => a === b, // custom equality — default is Object.is
  debugName: 'mySignal',
});

computed<T>(derive, options?)

Creates a lazy, memoized derived value. Recomputes only when a dependency changes and get() is called.

const greeting = computed(() => `Hello, ${name.get()}!`);

greeting.get();       // 'Hello, Bob!' — tracked read
greeting.peek();      // read without tracking
greeting.subscribe(fn); // subscribe — returns unsubscribe fn

Options:

computed(() => expensiveDerive(), {
  equals: (a, b) => a.id === b.id, // suppress downstream notification when equal
  debugName: 'myComputed',
});

effect(fn, options?)

Runs fn immediately and reruns whenever any signal read inside fn changes. Returns an EffectHandle with a dispose() method.

If fn returns a function, that function is called as cleanup before each rerun and on final disposal.

const handle = effect(() => {
  document.title = `Count: ${count.get()}`;
  return () => { /* cleanup before next run */ };
});

handle.dispose(); // stop the effect, run final cleanup

Options:

effect(fn, { debugName: 'titleEffect' });

batch<T>(fn)

Defers all signal notifications until fn completes, coalescing multiple writes into a single flush. Returns the value returned by fn. Nested batches are supported.

const result = batch(() => {
  a.set(1);
  b.set(2);
  c.set(3);
  return 'done';
}); // subscribers notified once; result === 'done'

untracked<T>(fn)

Executes fn without registering any signal reads as dependencies. Use this inside computed or effect to read a signal without tracking it.

const a = signal(1);
const b = signal(10);

effect(() => {
  const val = a.get();                    // tracked — effect reruns when a changes
  const snapshot = untracked(() => b.get()); // NOT tracked — b changes won't rerun the effect
  console.log(val, snapshot);
});

flush()

Force-processes any pending batched notifications synchronously. Useful in adapters and tests.

flush();

isSignal(value) / isWritableSignal(value)

Type guards for duck-typing signal instances.

import { isSignal, isWritableSignal } from '@web-loom/signals-core';

isSignal(signal(0));           // true
isSignal(computed(() => 1));   // true
isSignal(42);                  // false

isWritableSignal(signal(0));           // true
isWritableSignal(signal(0).asReadonly()); // false
isWritableSignal(computed(() => 1));   // false

Encapsulation pattern

Use asReadonly() to expose state without allowing external writes — mirrors Angular's encapsulation pattern.

class CounterViewModel {
  private _count = signal(0);

  readonly count = this._count.asReadonly();
  readonly doubled = computed(() => this._count.get() * 2);

  increment() {
    this._count.update((v) => v + 1);
  }
}

Design notes

  • Zero dependencies — no RxJS, no external runtime
  • Lazy computed — derived values only recompute on get() after a dependency changes; never recomputes eagerly
  • Dynamic dependency tracking — only signals actually read during a computation are tracked; stale deps are cleared automatically
  • Custom equality — both signal() and computed() accept an equals option; write is a no-op when equals(prev, next) returns true
  • Effect cleanup — returning a function from an effect registers it as cleanup, called before each rerun and on dispose()
  • Batching — nested batch() calls are safe; the flush happens once at the outermost boundary