Docs / Core / Signals & Effects
ven.js
Signals & Effects
Fine-grained reactivity with automatic dependency tracking — no virtual-DOM diffing of the whole tree.
venjs.signal(value)
Creates a reactive container around a value. Reading .value inside a running effect subscribes that effect; writing .value notifies all subscribers.
const count = venjs.signal(initialValue);
| Member | Description |
|---|---|
get value | Returns the current value and, if an effect is active, registers it as a dependency. |
set value(new) | Assigns a new value. If it is strictly equal (===) to the old one, nothing happens. Otherwise all subscribers re-run. |
peek() | Returns the value without registering a dependency — useful in event handlers or loops. |
const count = venjs.signal(0);
count.value; // 0
count.peek(); // 0 (no tracking)
count.value = 5; // notifies subscribers
count.value = 5; // ignored: same value
Equality: A signal only re-triggers when the new value is not
=== to the previous one. For objects/arrays, replace the reference (list.value = [...list.value, x]) to trigger updates.venjs.effect(fn)
Runs fn immediately, tracking every signal it reads. When any of those signals change, the effect stops its previous run (running cleanup), then re-runs. Returns a stop() function to dispose it.
const stop = venjs.effect(() => { /* ... */ return cleanupFn; });
| Behavior | Description |
|---|---|
| Tracking | Only signals read during the run are tracked. Reading a signal later (e.g. in a setTimeout) is not tracked. |
| Cleanup | If fn returns a function, it is called before the next re-run and when the effect is stopped. |
| Dispose | Call the returned stop() to unsubscribe and run the latest cleanup. |
const timer = venjs.signal(0);
let intervalId = null;
const stop = venjs.effect(() => {
clearInterval(intervalId); // cleanup from last run
const ms = timer.value;
intervalId = setInterval(() => console.log(ms), ms);
return () => clearInterval(intervalId); // explicit cleanup
});
timer.value = 1000; // effect re-runs with new value
stop(); // tears down, clears interval
Putting it together with render
The canonical pattern: render inside an effect so the UI stays in sync with signals. In a multi-page app each page component in components/<page>.js uses signals for its local state.
// components/counter.js
const Counter = () => {
const count = venjs.signal(0);
return venjs.div({}, [
venjs.p({}, `Count: ${count.value}`),
venjs.button({ onclick: () => count.value++ }, "Increment")
]);
};
window.Counter = Counter;
Lifecycle of an effect
- On run: previous subscriptions are cleared,
activeEffectis set,fn()executes and reads signals. - On signal change: the subscriber set re-runs
fn, first invoking the previous cleanup. - On stop: all dependencies are released and the last cleanup runs.
Common pitfall: reading a signal outside an effect (e.g. at module top-level) will not be tracked. Always read signals inside the render/effect closure you want to react.