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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@embra/reactivity

v0.0.7

Published

A lightweight, composable and explicit reactivity system.

Readme

@embra/reactivity

Docs Build Status npm-version Coverage Status

A lightweight, composable and explicit reactivity system.

Features

⚡ Plain reactivity. No proxies, no magic.

It does not convert the value with Object.defineProperty nor Proxy. Keeping everything as plain JavaScript value makes it easier to work with other libraries and easier for the JavaScript engine to optimize.

import { writable, readable, type Readable } from "@embra/reactivity";

const count$ = writable(0);
count$.set(1);

const [count2$, setCount] = readable(0);
setCount(1);

🔍 Explicit reactivity. No hidden dependencies, no surprises.

Unlike signal-based libraries, @embra/reactivity does not automatically track dependencies. You explicitly define what to watch and how to react to changes. This is easier to reason about dependencies and also reduce the cost of complex implicit dependency calculation.

With React hook-like API, computations can be pure functions which is more compatible with general non-reactive functions.

import { writable, derive } from "@embra/reactivity";

const count$ = writable(0);

const isPositive = (value: number): boolean => value > 0;

const positiveCount$ = derive(count$, isPositive);

Dynamic dependencies can be collected using get in compute or watch.

import { writable, compute, watch } from "@embra/reactivity";

const count$ = writable(0);
const doubleCount$ = compute(get => get(count$) * 2);

watch(get => {
  const count = get(count$);
  console.log(`Count is ${count}, double is ${get(doubleCount$)}`);
});

🛡️ Zero-cost ownership model. Type-safe lifecycle management.

In practice, one of the biggest problems we encountered with reactivity libraries is the lifecycle management of reactive values. In @embra/reactivity, the reactive dependencies are weakly referenced, so generally you don't need to manually dispose of them. However, explicit lifecycle management is still a good practice to avoid unexpected behaviors.

@embra/reactivity provides a zero-cost ownership model that allows you to create reactive values with explicit ownership.

By default, created reactive values are with type OwnedReadable or OwnedWritable, which exposes a dispose() method to clear their subscribers. When passing the reactive value to other modules, you can use Readable or Writable types to hide the dispose() method, ensuring that the value is not disposed of accidentally.

import { writable, readable, type Readable, type OwnedWritable } from "@embra/reactivity";

const count$: OwnedWritable<number> = writable(0);
count$.set(1);

// Hide the setter by typing
function logCount(count$: Readable<number>) {
  count$.subscribe(console.log);

  // @ts-expect-error
  count$.set(2);
}
logCount(count$);

// Hide the setter in runtime
const [count2$, setCount] = readable(0);
setCount(1);

// @ts-expect-error
count2$.set(2);

🧩 Flexible abstractions of state and actions.

In the days of Flux reducer model, we often used a single store to hold the state and actions to mutate the state. This was nice for reasoning about the state, but it also introduced a lot of boilerplate code.

Later on, a pattern with state and action glued together was introduced, like redux-actions. @embra/reactivity takes this a step further by providing a simple and flexible abstraction of state and actions.

In the following example, we create a Writable count$ which looks like a Writable<number>, but internally it is derived from a larger application state appState$. This allows other modules to depend on a Writable<number> without knowing the details of the application state.

import { writable, derive, toWritable } from "@embra/reactivity";
import { trace } from "@embra/reactivity/debug";

const appState$ = writable({
  count: 0,
  user: null,
});

const count$ = toWritable(
  derive(appState$, state => state.count),
  count => appState$.set({ ...appState$.value, count }),
);

// when debugging, you can trace the reactive value
trace(count$);

⏳ Scheduler mechanism for controlled updates.

@embra/reactivity includes a scheduler mechanism and built-in schedulers that lets you control when reactive updates are processed. This is useful for batching updates and deferring computations.

import { writable, MicrotaskScheduler } from "@embra/reactivity";

const rapidChangeCount$ = writable(0);

rapidChangeCount$.reaction(console.log, MicrotaskScheduler);

count$.set(1);
count$.set(2);

await Promise.resolve();
// Logs "2" once after a microtask tick, reducing unnecessary computations.

You can also provide your owned custom scheduler function easily.

import { writable, asyncScheduler } from "@embra/reactivity";

const MicrotaskScheduler = asyncScheduler(flush => Promise.resolve().then(flush));
const AnimationFrameScheduler = asyncScheduler(requestAnimationFrame);

🏗️ Framework agnostic. First-class support for React.

@embra/reactivity is designed to be framework agnostic. It can be used with any framework or library that supports JavaScript. It also provides first-class support for React.

  • Reactive values as component props are just simpler and more efficient.
    • value = Readable
    • value + onChange = Writable
    • state + action = toWritable(derive(rootState$, selector), action) (See State and Actions)
  • A lot of re-renderings can be avoided when passing reactive values down the props. You only need to subscribe to the reactive values that are actually used in the component.
import { writable } from "@embra/reactivity";
import { useValue, useDerived, useCombined } from "@embra/reactivity/react";

const count$ = writable(0);

function Counter({ count$ }) {
  const count = useValue(count$);
  const countDouble = useDerived(count$, count => count * 2);
  return (
    <div>
      <button onClick={() => count$.set(count - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => count$.set(count + 1)}>+</button>
      <span>{countDouble}</span>
    </div>
  );
}

📦 Small bundle size. Focused on performance and simplicity.

export size

Install

npm add @embra/reactivity

Debugging

trace

@embra/reactivity provides a trace() function to help debug reactive values and watches. It tracks value and dependency changes and logs them to the console.

import { writable, watch } from "@embra/reactivity";
import { trace } from "@embra/reactivity/debug";

const count$ = writable(0);

// trace a reactive value
trace(count$);

// trace a watch function
watch(trace(get => get(count$)));

count$.set(1);

Chrome Devtools Custom Formatter

@embra/reactivity supports Chrome DevTools custom formatters. You may enable it by checking the "Enable custom formatters" option in the "Console" section of DevTools general settings.

It is enabled in development by default. You can also enable it manually by calling customFormatter().

import { customFormatter } from "@embra/reactivity/debug";
customFormatter();