actzs
v0.0.3
Published
class-based reactive UI library with fine-grained dependency tracking and no VDOM
Maintainers
Readme
actzs
A tiny, class-based reactive UI library with explicit mutation tracking, full call-stack attribution, and a fine-grained DOM layer — no virtual DOM, no diffing.
npm install actzsimport { Ref, Computed, h, mount, setRef, runNamed } from 'actzs'
const count = new Ref(0)
const doubled = new Computed(() => count.value * 2)
function App() {
function increment() {
setRef(count, count.value + 1)
}
return h('div', {}, [
h('p', {}, ['Count: ', count]),
h('p', {}, ['Doubled: ', doubled]),
h('button', { onClick: () => runNamed(increment) }, '+1'),
])
}
mount(h(App), document.getElementById('app'))Why actzs?
Most reactive UI frameworks use a virtual DOM and diffing. actzs takes a different approach:
- Fine-grained reactivity — every reactive read is tracked; when a value changes, only the exact DOM nodes / effects that depend on it re-run.
- Full traceability — every mutation records its caller stack. Open the console and ask "who changed this, and why?"
- No VDOM, no diffing —
idombinds effects directly to real DOM nodes. Zero wasted comparisons. - Zero dependencies — no framework, no build step required. Just ESM imports.
Core API
| Concept | Type | Import |
|---|---|---|
| Primitive ref | Ref | import { Ref } from 'actzs' |
| Reactive object/array/Map | Reactive | import { Reactive } from 'actzs' |
| Derived value | Computed | import { Computed } from 'actzs' |
| Side effect | Effect.run / watch | import { Effect, watch } from 'actzs' |
| Set ref value | setRef(ref, value) | import { setRef } from 'actzs' |
| Set reactive property | setReactive(proxy, key, value) | import { setReactive } from 'actzs' |
| Call reactive method | setReactiveMethod(proxy, method, ...args) | import { setReactiveMethod } from 'actzs' |
| Named call context | runNamed(fn, ...args) | import { runNamed } from 'actzs' |
| Named callback wrapper | named(fn) | import { named } from 'actzs' |
| Async context capture | captureContext() | import { captureContext } from 'actzs' |
| VNode factory | h(tag, props?, children?) | import { h } from 'actzs' |
| Mount to DOM | mount(vnode, container) | import { mount } from 'actzs' |
| Reactive mount | reactiveMount(fn, container) | import { reactiveMount } from 'actzs' |
| Lifecycle hooks | onMount(fn) / onDestroy(fn) | import { onMount, onDestroy } from 'actzs' |
| Reactive list | runList(parent, items) | import { runList } from 'actzs' |
| Graph inspector | Graph | import { Graph } from 'actzs' |
How it works
Reactive data
const score = new Ref(0) // primitive
const player = new Reactive({ x: 0, y: 0 }) // object / array / Map
const doubled = new Computed(() => score.value * 2) // derived- Read with
.value(Ref/Computed) or direct property access (Reactive). - Direct assignment throws — always use setters.
Mutations
setRef(score, 10)
setReactive(player, 'x', 5)
setReactiveMethod(items, 'push', { id: 3 })Every mutation records who triggered it via the call context stack.
Named call context
Wrap mutating functions so the graph knows the caller:
function addScore(points) {
setRef(score, score.value + points)
}
runNamed(addScore, 10) // graph: by ['addScore']
button.onclick = () => runNamed(addScore, 10)
// Async: capture the context before scheduling
function startTimer() {
const ctx = captureContext()
setInterval(ctx(() => setRef(t, t.value - 1)), 1000)
}
runNamed(startTimer) // graph: by ['startTimer']DOM with idom
Pass Refs directly to h() — they stay reactive:
h('p', {}, ['Score: ', score]) // reactive text
h('div', { style: { opacity: opacityRef } })
h('img', { src: imageUrlRef })No VDOM diffing. Each Ref-bound prop/text spins up its own Effect that updates the real DOM node directly.
Debugging
const g = new Graph()
window.g = g
window.score = score
g.dump() // all effects + recent mutations
g.lookup(score, 'value') // what depends on score.valueCoding with actzs via LLM
actzs is designed to be LLM-friendly. The full project rules are in:
GUIDE.md— complete developer guide including an LLM prompt template (section 10) that you can copy-paste when asking an LLM to write actzs code.AGENTS.md— machine-readable rules that tools like opencode use to enforce actzs conventions.
Key rules the LLM prompt enforces:
- Every mutable variable must be
ReforReactive— nolet. - Every mutation uses a setter — no direct assignment.
- Every mutating function is wrapped in
runNamed/named/captureContext. - Every text/attribute in
h()is aReforComputed— no string literals. - Everything is exposed on
windowfor graph debugging.
Games built with actzs
| Game | Stack | |---|---| | Flappy Bird | Canvas 2D, physics, collision | | Helix Fall | Three.js tower descent | | Helix Fall 2 | Three.js neon tower + combos | | Cut the Rope | Verlet physics, swipe cutting | | Red Ball 4 | Platformer, physics enemies | | Angry Birds | Slingshot projectile, destructible blocks | | Category Sort | 3D sorting with Three.js | | Jigsaw Solitaire | Tile matching puzzle | | Jump n Roll | Endless runner |
All games expose window.g — open DevTools and type g.dump() to see the full reactivity graph.
License
MIT
