@pfeiferio/ref
v1.0.0
Published
Minimal mutable reference primitive with change notifications
Downloads
11
Maintainers
Readme
@pfeiferio/ref
A minimal mutable reference primitive with change notifications.
A small, explicit Ref abstraction for holding and observing mutable state.
It is not a reactive framework and intentionally avoids schedulers, dependency tracking, or magic.
Features
- Explicit mutable references
- Change notifications via
on/off/once - Deep equality comparison — no event fired if value did not change
- Recursive
unref()unwrapping [Symbol.dispose]support for explicit cleanup- No dependencies (Node.js only)
Installation
npm install @pfeiferio/refBasic Usage
import {ref} from '@pfeiferio/ref'
const count = ref(0)
count.on('update', (next, prev) => {
console.log(prev, '→', next)
})
count.value = 1 // 0 → 1
count.value = 1 // no event — value unchangedref()
Creates a new Ref instance. Always returns a new instance — no idempotency, no implicit aliasing.
const a = ref(1)
const b = ref(1)
a === b // false — always distinct instancesIf a Ref is passed as value, it is unwrapped first:
const a = ref(1)
const b = ref(a) // Ref<number>, not Ref<Ref<number>>
b.value // 1
a.value = 99
b.value // 1 — b is independentIf you want to share state, pass the same instance around directly.
unref()
Unwraps a Ref to its value. If the value is not a Ref, it is returned as-is.
Unwrapping is recursive — nested Refs are fully resolved.
unref(1) // 1
unref(ref(1)) // 1
unref(ref(ref(ref(1)))) // 1isRef()
Type guard for checking whether a value is a Ref.
if (isRef(x)) {
console.log(x.value)
}Events
Ref exposes a minimal event API. Only 'update' is emitted, and only when the value actually changed.
Deep equality is used for objects, arrays, and dates.
const r = ref({a: 1})
const listener = (next, prev) => console.log(prev, '→', next)
r.on('update', listener)
r.once('update', listener)
r.off('update', listener)Ref does not extend EventEmitter — only these three methods are part of the public API.
Updates
Updates must always be applied by replacing the entire value. Direct mutation of nested objects will not trigger events.
const r = ref({a: {b: 1}})
r.value.a.b = 2 // silent — no event fired
r.value = {...r.value, a: {b: 2}} // fires eventCleanup
Ref implements Symbol.dispose for use with the using keyword (TypeScript 5.2+, Node.js 20+).
{
using r = ref(0)
r.on('update', () => { ...
})
} // all listeners removed automaticallyOr manually:
r[Symbol.dispose]()Semantics
ref()always creates a new instance- State sharing is always explicit — pass the same instance
- Copying state is always explicit — use
structuredCloneor spread - No implicit linking between
Refinstances - No schedulers, no dependency tracking, no magic
License
MIT
