Reactivity ​
Introduction ​
Reactivity is the mechanism by which the framework automatically updates the interface in response to state changes. Owl 3 provides a fine-grained reactivity system built on four primitives: signals, computed values, proxies, and effects.
A key design principle is that reactive values are not tied to components. They can be created anywhere — in a component, a plugin, or a plain JavaScript module — and shared freely across the application. Dependency tracking happens at read time: when a reactive value is read inside an effect or a component render, a subscription is created. When the value changes, all subscribers are notified and updates are batched via microtasks.
const count = signal(0);
const state = proxy({ color: "red", value: 15 });
const total = computed(() => count() + state.value);
console.log(total()); // 15The primitives ​
Each primitive has its own reference page:
- Signals — the basic reactive container: a value you read and write explicitly (
s()/s.set(v)). Collection and ref variants are covered there too. - Computed values — lazily-evaluated derived values that track their dependencies and recompute on demand, plus the asynchronous
asyncComputed. - Proxies — reactive objects where reading a property subscribes to it and writing notifies subscribers, with deep wrapping.
- Effects — functions that re-run when their reactive dependencies change, including the
useEffectcomponent hook.
How they tie together ​
The four primitives share a single dependency-tracking mechanism. Whenever a reactive value — a signal, a computed, or a proxy property — is read inside a tracking context (an effect, a computed, or a component render), a subscription is recorded. When that value later changes, every subscriber is invalidated and rescheduled.
This is why the primitives compose freely: a computed can read signals and proxy properties; an effect can read computeds; a component template is itself a tracking context that re-renders when any reactive value it read changes. None of them need to know about each other — they only need to be read and written through the reactive APIs.
const first = signal("John");
const last = signal("Doe");
const fullName = computed(() => `${first()} ${last()}`);
effect(() => console.log(fullName())); // logs "John Doe"
first.set("Jane"); // after a microtask, the effect logs "Jane Doe"Batching ​
All reactive updates are batched in microtasks. Multiple signal writes in the same synchronous block trigger only a single effect re-run:
const a = signal(1);
const b = signal(2);
effect(() => {
console.log(a() + b()); // logs 3
});
a.set(10);
b.set(20);
// only one re-run after the microtask — logs 30, not 12 then 30