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);
MemberDescription
get valueReturns 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; });
BehaviorDescription
TrackingOnly signals read during the run are tracked. Reading a signal later (e.g. in a setTimeout) is not tracked.
CleanupIf fn returns a function, it is called before the next re-run and when the effect is stopped.
DisposeCall 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, activeEffect is 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.