@embra/reactivity

中文文档
0.0.8发布于 2026/04/08MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 1.2万

A lightweight, composable and explicit reactivity system.

原始使用文档
文档目录12 节
  1. Features
  2. ⚡ Plain reactivity. No proxies, no magic.
  3. 🔍 Explicit reactivity. No hidden dependencies, no surprises.
  4. 🛡️ Zero-cost ownership model. Type-safe lifecycle managemen…
  5. 🧩 Flexible abstractions of state and actions.
  6. ⏳ Scheduler mechanism for controlled updates.
  7. 🏗️ Framework agnostic. First-class support for React.
  8. 📦 Small bundle size. Focused on performance and simplicity.
  9. Install
  10. Debugging
  11. trace
  12. Chrome Devtools Custom Formatter

@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();

版本与发布记录

@embra/reactivity 共发布 8 个版本,首次发布于 2025/08/02;过去 12 个月发布了 2 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.0.8latest2026/04/087 个月前
0.0.72025/11/1811 个月前
0.0.62025/11/051 年前
0.0.52025/11/051 年前
0.0.42025/09/031.2 年前
0.0.32025/08/271.2 年前

关于 @embra/reactivity 的常见问题

@embra/reactivity 是什么?

A lightweight, composable and explicit reactivity system.

如何安装 @embra/reactivity?

在项目目录运行 npm install @embra/reactivity,或使用 pnpm add / yarn add / bun add。

@embra/reactivity 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@embra/reactivity 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@embra/reactivity 可以商用吗?

@embra/reactivity 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@embra/reactivity 的下载量有多少?

根据 npm 官方下载统计,@embra/reactivity 在 2026/10/14 – 2026/11/12 这 30 天内被下载 12,290 次,日均约 410 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@embra/reactivity 还在维护吗?

最新版本 0.0.8 发布于 2026/04/08(7 个月前);至今共发布 8 个版本,其中过去 12 个月发布了 2 个;首次发布于 2025/08/02。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。