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

@sigrea/core

v0.5.0

Published

The signal base reactive programming library.

Readme

@sigrea/core

Sigrea is a small reactive core built on alien-signals. It adds deep reactivity and scope-based lifecycles. It provides core primitives to build hooks, plus optional lifecycles for ownership and cleanup.

  • Core primitives. signal, computed, deepSignal, watch, and watchEffect.
  • Lifecycles. Scope, onMount, and onUnmount for cleanup boundaries.
  • Molecules. molecule() is a lifecycle container that doesn't render UI.
  • Composition. Build molecule trees via get().
  • Testing. trackMolecule + disposeTrackedMolecules helps reproduce lifecycles in tests.

Inspired by:

  • Vue 3 — deep reactivity and scope control
  • nanostores — store-centric architecture
  • bunshi — molecule and composition API design

Table of Contents

Install

npm install @sigrea/core

Adapters

Official adapters connect Sigrea molecules and signals to UI frameworks:

  • @sigrea/vue — Vue 3.4+ composables (useMolecule, useSignal, useMutableSignal, useDeepSignal)
  • @sigrea/react — React 18+ hooks (useMolecule, useSignal, useComputed, useDeepSignal)

Each adapter binds molecule lifecycles to component lifecycles and synchronizes signal subscriptions with the framework's reactivity system.

Quick Start

Signals and Computed

import { computed, signal } from "@sigrea/core";

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

count.value = 3;
console.log(doubled.value); // 6

Hooks

Hooks are plain functions built from the core primitives. This package does not include UI bindings. In UI apps, you usually call hooks inside a molecule. Then connect the molecule to the UI layer via an adapter.

Example: state + actions

import { computed, readonly, signal } from "@sigrea/core";

export function useCounter(initial = 0) {
  const count = signal(initial);
  const doubled = computed(() => count.value * 2);

  const increment = () => {
    count.value++;
  };

  const decrement = () => {
    count.value--;
  };

  return {
    count: readonly(count),
    doubled,
    increment,
    decrement,
  };
}

Example: deepSignal for nested state

import { computed, deepSignal } from "@sigrea/core";

export function useUserProfile() {
  const profile = deepSignal({
    name: "Mendako",
    address: { city: "Tokyo" },
  });

  const label = computed(() => {
    return `${profile.name} @ ${profile.address.city}`;
  });

  const setCity = (city: string) => {
    profile.address.city = city;
  };

  return {
    profile,
    label,
    setCity,
  };
}

Molecules

molecule(setup) creates a function. Calling it creates a new instance with its own root Scope. It does not render anything. Use molecules when you need:

  • a clear ownership + cleanup boundary (Scope, onUnmount),
  • parent-child relationships between lifecycled units (get()),
  • per-instance initial configuration via props.

Props are meant to be immutable configuration. Sigrea does not track prop changes. If you need dynamic inputs, model them via signals or explicit molecule methods.

Molecule setup only constructs state. When onMount, onUnmount, watch, or watchEffect are called during setup, their work is deferred until the molecule is mounted. Official adapters mount and unmount molecules automatically. If you use the core package directly, call mountMolecule() and unmountMolecule().

Inside setup, you can call hooks or use the core primitives directly. Child molecules are internal dependencies—prefer returning only the outputs (signals, computed values, actions) that consumers need.

Creating a molecule

import { molecule, onMount, onUnmount, readonly, signal } from "@sigrea/core";

interface IntervalMoleculeProps {
  intervalMs: number;
}

const IntervalMolecule = molecule<IntervalMoleculeProps>((props) => {
  const tick = signal(0);
  let id: ReturnType<typeof setInterval> | undefined;

  onMount(() => {
    id = setInterval(() => {
      tick.value += 1;
    }, props.intervalMs);
  });

  onUnmount(() => {
    if (id === undefined) {
      return;
    }
    clearInterval(id);
  });

  return {
    tick: readonly(tick),
  };
});

Composing molecules with get()

import { get, molecule, readonly, signal, watch } from "@sigrea/core";

interface DraftSessionMoleculeProps {
  intervalMs: number;
  initialText: string;
  save: (text: string) => void;
}

export const DraftSessionMolecule = molecule<DraftSessionMoleculeProps>(
  (props) => {
    const text = signal(props.initialText);
    const isDirty = signal(false);

    const setText = (next: string) => {
      text.value = next;
      isDirty.value = true;
    };

    const save = () => {
      props.save(text.value);
      isDirty.value = false;
    };

    const interval = get(IntervalMolecule, {
      intervalMs: props.intervalMs,
    });

    watch(interval.tick, () => {
      if (!isDirty.value) {
        return;
      }
      save();
    });

    return {
      isDirty: readonly(isDirty),
      setText,
      save,
      text: readonly(text),
    };
  },
);

Notes:

  • get() must be called synchronously during molecule setup.
  • onUnmount() callbacks and watch() effects are tied to the mount lifecycle.
  • Child molecules created via get() are disposed with their parent.

Testing

// tests/CounterMolecule.test.ts
import { afterEach, expect, it } from "vitest";

import {
  disposeTrackedMolecules,
  molecule,
  readonly,
  signal,
  trackMolecule,
} from "@sigrea/core";

afterEach(() => disposeTrackedMolecules());

it("increments and exposes derived state", () => {
  const CounterMolecule = molecule(() => {
    const count = signal(10);

    const increment = () => {
      count.value++;
    };

    return {
      count: readonly(count),
      increment,
    };
  });

  const counter = CounterMolecule();
  trackMolecule(counter);
  counter.increment();

  expect(counter.count.value).toBe(11);
});

Handling Scope Cleanup Errors

Cleanup callbacks run when a scope is disposed. If a cleanup throws, Sigrea collects errors into an AggregateError.

Async cleanups are not awaited. If an async cleanup rejects, Sigrea forwards the error to the handler (if any). In dev, Sigrea also logs the rejection.

Use setScopeCleanupErrorHandler to customize error handling. This is useful for logging or reporting to monitoring services.

import { setScopeCleanupErrorHandler } from "@sigrea/core";

setScopeCleanupErrorHandler((error, context) => {
  console.error(`Cleanup failed:`, error);

  // Forward to monitoring service
  if (typeof Sentry !== "undefined") {
    Sentry.captureException(error, {
      tags: { scopeId: context.scopeId, phase: context.phase },
    });
  }
});

The handler receives error and context. context includes scopeId, phase, index, and total.

Return ScopeCleanupErrorResponse.Suppress to prevent the error from being thrown. Return ScopeCleanupErrorResponse.Propagate to rethrow immediately for synchronous errors.

Development

This repo targets Node.js 20 or later.

Browser dev flag

Some dev-only diagnostics are guarded by __DEV__. In Node.js, Sigrea uses process.env.NODE_ENV !== "production".

In browsers, you can override this at build time by defining a global constant __SIGREA_DEV__ with your bundler. If you don't define it, __DEV__ defaults to false in browsers.

Vite example:

// vite.config.ts
import { defineConfig } from "vite";

export default defineConfig(({ command }) => ({
  define: {
    __SIGREA_DEV__: command === "serve",
  },
}));

If you use mise:

  • mise trust -y — trust mise.toml (first run only).
  • mise run ci — run CI-equivalent checks locally.
  • mise run notes — preview release notes (optional).

You can also run pnpm scripts directly:

  • pnpm install — install dependencies.
  • pnpm test — run tests.
  • pnpm typecheck — run TypeScript type checking.
  • pnpm test:coverage — collect coverage.
  • pnpm build — build the package.
  • pnpm cicheck — run CI checks locally.

See CONTRIBUTING.md for workflow details.

License

MIT — see LICENSE.