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

@frada/tally

v2.0.0

Published

Keep a tally of data reaction

Readme

为 React 18 / 19 设计的小而快的响应式层 —— signals + reactions + Zustand 风格 store,产物小于 4 KB。

English

npm install @frada/tally   # 或 yarn / pnpm add @frada/tally

快速开始:Zustand 风格的 store

import { create } from '@frada/tally'

const bearStore = create({ bears: 0 })

const { useStore, dispatch, getState, subscribe, dispose } = bearStore

export const increasePopulation = () => dispatch((state) => ({ bears: state.bears + 1 }))
export const removeAllBears     = () => dispatch({ bears: 0 })

export const useBearStore = useStore
export const bearDispatch = dispatch
function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  return (
    <div>
      <button onClick={increasePopulation}>one up</button>
      <button onClick={removeAllBears}>remove all</button>
    </div>
  )
}

无需 Provider。组件只在所选切片变化时 re-render。


API 参考

create<T>(initState)

返回一个 store,成员如下:

| 成员 | 签名 | 说明 | |---|---|---| | useStore | <S>(selector?, equalityFn?) => S | React hook。选取切片。默认 Object.is。 | | dispatch | (partial \| producer) => void | 对象 → 浅合并;函数 → Immer producer(改 draft)。 | | getState | () => T | React 外读取当前状态,不订阅。 | | subscribe | (listener) => unsubscribe | React 外订阅。每次变化都拿到最新 state。 | | dispose | () => void | 永久禁用 store。dispatch 变 no-op;外部订阅者全部解除。幂等。 |

// selector + 自定义相等(浅等时不 re-render)
import { shallow } from '@frada/tally'
const { name, age } = useStore((s) => ({ name: s.name, age: s.age }), shallow)

// 外部订阅
const unsub = bearStore.subscribe((state) => console.log('changed', state))
unsub()

// 一次性读取
const snapshot = bearStore.getState()

// 动态创建的 store 卸载时清理
bearStore.dispose()

useShallow(selector)

包装 selector,使浅等的输出复用上次的引用。inline 对象 selector(否则每次都 re-render)最干净的修复。

import { useShallow } from '@frada/tally'

const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })))

大多数场景等价于 useStore(selector, shallow),而且引用跨渲染稳定。


createSignal<T>(initialValue, options?)

响应式原子。返回 [read, write]

import { createSignal, shallow } from '@frada/tally'

const [count, setCount] = createSignal(0)
count()                  // 0
setCount(1)              // 1
setCount((prev) => prev + 1)

// 自定义相等 —— 仅在浅不等时通知订阅者
const [points, setPoints] = createSignal([1, 2, 3], { equals: shallow })
setPoints([1, 2, 3])     // 不通知 —— shallow-equal

useSignal<T>(initialValue)

组件本地 signal(与 useState 同形,但返回 signal 对)。

function Counter() {
  const [count, setCount] = useSignal(0)
  return <button onClick={() => setCount(count() + 1)}>{count()}</button>
}

useReaction<T, S>(fn, selector?, equalityFn?)

useStore 更底层 —— 订阅任意"读 signal 的函数"。

const value = useReaction(myMemo)                                    // 订阅一个 memo
const slice = useReaction(myStore, (s) => s.list, shallow)          // selector + 相等函数

内部细节:

  • getSnapshot 只在 fn / selector / equalityFn 变化时重建(React 19 concurrent-safe)
  • subscribe 稳定 —— 跨渲染不重订阅
  • selector 切换时,若 equalityFn 命中则保留上次引用,下游 useMemo / React.memo 链不会被打破

createMemo<T>(compute, options?)

派生 signal。追踪 compute 内读到的所有 signal;依赖变化时重算;equals 命中时不通知下游。

import { createSignal, createMemo, useReaction } from '@frada/tally'

const [first, setFirst] = createSignal('Ada')
const [last,  setLast]  = createSignal('Lovelace')

const fullName = createMemo(() => `${first()} ${last()}`)

function NameTag() {
  const name = useReaction(fullName)
  return <span>{name}</span>
}

// 模块级 memo 不再使用时应 dispose
fullName.dispose()
  • lazy init:首次有人读 memo 时才跑 compute
  • eager 重算:首次 read 后,依赖变化在下一个 scheduler tick 自动重算
  • equality 短路:重算结果与上次 equals 命中时,下游订阅者不会被通知

createReaction() —— 低层原语

useReactioncreateMemo 背后的构建块。大多数用户不需要直接用。

const { track, reconcile, dispose } = createReaction()
reconcile(() => console.log('deps changed'))
track(() => { count(); name() })     // 建立依赖集合
dispose()

| 成员 | 说明 | |---|---| | track(fn) | 在 reaction 的追踪上下文中运行 fn。diff-based:只有"消失"的依赖会被解除订阅。 | | reconcile(fn) | 设置下次依赖变化后 scheduler flush 时调用的回调。 | | dispose() | 永久禁用。幂等。 |


untracked(fn)

读 signal 但不订阅

import { untracked } from '@frada/tally'

const snapshotOnly = untracked(() => count())   // 不建立依赖

autorun(effect)reaction(deps, effect, options?)

MobX 风格的副作用 API。两者都返回 dispose 函数。

import { autorun, reaction, createSignal } from '@frada/tally'

const [count, setCount] = createSignal(0)

// autorun:立即跑一次,后续任意 signal 变化都重跑
const stop1 = autorun(() => {
  document.title = `${count()} unread`
})

// reaction:仅 deps() 返回值变化才跑 effect
const stop2 = reaction(
  () => count(),
  (current, previous) => console.log(`从 ${previous} 变成 ${current}`),
  { equals: Object.is, fireImmediately: false, delay: 0 }, // 全部为默认
)

stop1()
stop2()

| API | effect 何时跑 | 追踪范围 | |---|---|---| | autorun(effect) | 立即 + 每次依赖变化 | effect 内所有 signal 都是 deps | | reaction(deps, effect, opts?) | 仅 deps() 返回新值时 | 只追踪 deps 内的 signal;effect 在 untracked 上下文运行 |

reactiondelay 选项(debounce 语义):

// dep 变化后等 200ms 才跑 effect;期间又变化重置计时
const stop = reaction(
  () => query(),
  (q) => fetch(`/search?q=${q}`),
  { delay: 200 },
)

configureScheduler(type)

全局切换批处理调度引擎。应用启动时调用一次。

import { configureScheduler } from '@frada/tally'

configureScheduler('channel')    // 默认 —— MessageChannel(宏任务,适合高频写入)
configureScheduler('promise')    // Promise.resolve().then(微任务,响应最快)
configureScheduler('microtask')  // queueMicrotask

setErrorHandler(handler)

抛错的 reaction 被隔离(不影响队列其他 reaction)。默认行为是异步重抛,让宿主的 unhandled-error handler 捕获。生产环境推荐注入自定义 handler,转发到 Sentry / 监控平台。

import { setErrorHandler } from '@frada/tally'

setErrorHandler((err) => Sentry.captureException(err))
setErrorHandler(null) // 恢复默认

shallow(a, b)

浅相等 helper,支持对象、数组、Map、Set。作为 equalityFn 使用:

useStore((s) => ({ a: s.a, b: s.b }), shallow)

设计要点

  • Concurrent-safe:useReaction 用 lazy useRef 初始化 + commit-phase useEffect 同步,React 19 strict-mode 双 mount 与 selector 切换都能保持引用稳定。
  • 批处理调度:同一 tick 内的多次写入合并;默认 flush 走 MessageChannel(宏任务),避免阻塞输入处理。
  • Diff-based 依赖追踪:每次 track 周期拿到一个新的 runIdsubscribeDeprunId 在 O(1) 内 dedup;track 的 finally 只移除"本次未出现"的旧依赖。稳定依赖路径下零 Set 写入。
  • 错误隔离:单个抛错的 reaction 经 errorHandler 上报,绝不污染队列其余 reaction。

工程

pnpm test       # 跑全部测试(44 个)
pnpm bench      # 跑微基准
pnpm build      # 构建到 dist/(CJS + ESM + d.ts)
pnpm lint       # eslint

参见 CHANGELOG.md 了解版本历史。


为什么用 @frada/tally 而不是 redux?

  • hook 是消费状态的主要方式 —— 不用 connect,无样板代码
  • 不需要把整个 app 包在 context provider 里
  • 压缩后 < 4 KB

为什么用 @frada/tally 而不是裸 context?

  • 重新渲染所选切片变化了的组件
  • 自带 subscribe,可在 React 之外使用
  • 内置 createMemo 派生状态