@rrjs/signals
v0.1.2
Published
Fine-grained reactive primitives: createSignal, computed, effect, batch
Maintainers
Readme
@rrjs/signals
The reactive primitive layer. Used by every other package in the Reactive React project. Can be used directly when you need fine-grained reactivity without a UI library.
Install
npm install @rrjs/signalsQuick start
import { createSignal, effect, computed } from '@rrjs/signals'
const [count, setCount] = createSignal(0)
const doubled = computed(() => count() * 2)
effect(() => {
console.log(`count=${count()}, doubled=${doubled()}`)
})
// → "count=0, doubled=0"
setCount(5)
// → "count=5, doubled=10"API
createSignal(initial)
Returns [getter, setter]. Reading the getter inside an effect or computed subscribes that effect to changes.
const [name, setName] = createSignal('Alice')
name() // 'Alice'
setName('Bob')
name() // 'Bob'The setter accepts a functional updater:
setName(prev => prev.toUpperCase())Same-value writes use Object.is and skip notification.
computed(fn)
A derived signal. Returns a getter. The function re-runs only when its tracked dependencies change.
const [a, setA] = createSignal(2)
const [b, setB] = createSignal(3)
const sum = computed(() => a() + b())
sum() // 5
setA(10)
sum() // 13Handles diamond dependencies correctly — a downstream computed fires exactly once when a shared upstream signal changes.
effect(fn)
Runs fn immediately, then re-runs it whenever any signal read inside changes.
const dispose = effect(() => {
console.log(count())
return () => console.log('cleanup') // optional cleanup
})
dispose() // stop re-running, run final cleanupReturns a dispose function. Cleanups run in reverse order: previous-cleanup → next-effect-run.
batch(fn)
Defers effect flushing until fn returns. Multiple signal writes inside batch produce one flush.
batch(() => {
setA(1)
setB(2)
setC(3)
})
// Effects depending on a, b, c run once, not three timesEdge cases
- Async tracking: signals read inside
setTimeout,await, orrequestAnimationFramecallbacks do not track. The observer stack is synchronous. - Cycles: writing to a signal you're currently tracking is allowed but may cause unintended re-runs. The library does not detect cycles automatically.
- Same-value writes:
setCount(5); setCount(5)only notifies once. UsesObject.is, soNaNcompared with itself is equal, and+0and-0are not.
License
MIT
