The model in ten seconds
An observable holds one value, read and written through .value. A computed is a function whose inputs are found
at runtime: it reads what it reads, and recalculates only when one of those things changes. An effect is the same
machinery pointed at a side effect. Every binding on a page is an effect.
0
- Doubled
- 0
- Parity
- even
- Effect runs
- 1
Nothing else on this page re-rendered. Three bindings read count; three bindings ran.
/**
* Counter - `observable`, `computed`, `effect`, and what "fine-grained" means.
*
* Three bindings read `count`. Pressing +1 re-runs those three and nothing
* else on the page, because each binding owns its own effect and each effect
* knows exactly what it read.
*/
function counter() {
const count = observable(0);
const runs = observable(0);
// A computed works out its own inputs at runtime. Neither of these is told
// that it depends on `count`; both discover it by reading it.
const doubled = computed(() => count.value * 2);
const parity = computed(() => (count.value % 2 === 0 ? 'even' : 'odd'));
// An effect is the same machinery pointed at a side effect. `peek()` reads
// without subscribing - reading `runs.value` here would make this effect
// depend on its own write and spin forever.
effect(() => {
count.value;
runs.value = runs.peek() + 1;
});
return {
count, doubled, parity, runs,
step(by) { count.value += by; },
reset() { count.value = 0; }
};
}doubled is never told it depends on count - it finds out by reading it. Writes are batched: however many you make
in a row, the graph settles once, on the next microtask.
A computed you can write to
A computed is read-only unless you say where a write should land. Give it a write and it becomes a derived value you
can bind two ways - here, Fahrenheit has no state of its own at all.
Type in either box. fahrenheit holds nothing of its own - it is a computed with a write, so the typing lands in celsius, and it reads as: mild.
/**
* A writable computed - a derived value you can bind two ways.
*
* `fahrenheit` has no state of its own. Reading it converts `celsius`;
* writing it converts back and stores the result in `celsius`. That is what
* lets `data-model="fahrenheit.value"` sit on an input at all: without a
* `write`, the typing would land on a cached read and vanish.
*/
function temperature() {
// The one piece of state, held at full precision.
const celsius = observable(20);
const round = (n) => Math.round(n * 10) / 10;
// Both inputs bind to writable computeds. Each rounds only what it SHOWS
// and writes back exactly, so a value typed into either box reads back
// unchanged and never fights the typing.
const celsiusShown = computed({
read: () => (celsius.value === null ? null : round(celsius.value)),
write: (c) => { celsius.value = c; }
});
const fahrenheit = computed({
read: () => (celsius.value === null ? null : round(celsius.value * 9 / 5 + 32)),
write: (f) => { celsius.value = f === null ? null : (f - 32) * 5 / 9; }
});
const feel = computed(() => {
const c = celsius.value;
if (c === null) return 'type a temperature';
if (c < 5) return 'cold - coat on';
if (c < 18) return 'mild';
if (c < 26) return 'warm';
return 'hot - find some shade';
});
return {celsius: celsiusShown, fahrenheit, feel};
}Without the write, data-model="fahrenheit.value" would assign onto a cached read and every keystroke would vanish on
the next recompute - so assigning to a read-only computed warns instead, naming it.
The rules worth knowing
.value everywhere |
The same spelling in JavaScript and in a template. There is no unwrapping magic - see safety. |
peek() |
Read without registering a dependency - how an effect reads a value it must not react to. |
| The change gate | A write always lands; notification happens only when the value actually changed (deep equality by default). Mutating an object in place and assigning it back is invisible - produce a new value. |
observableArray |
push, splice, remove, sort and the rest notify; .length and indexOf are tracked. |
subscribe |
Fires synchronously at the write, for code that wants a callback rather than a binding. |
| Disposal | Every entry point returns something to tear down with - dispose(), destroy() or off(). |
Full detail: the reference.