docs / Runtime
track
The reactive primitive: a cell you read, write and subscribe to — with a binding you use like a plain variable.
track(init) creates a CELL: a box holding a value the compiler knows how to re-read. Nothing polls it, nothing diffs the DOM — a write notifies exactly the places that read the cell, and those places re-run. That is why the runtime you ship is small: reactivity is compile-time knowledge, not a subscription graph walked at render time.
You almost never touch the cell itself. The &[ destructuring gives you a BINDING that you read and write like any other variable — count++, count = 5, count === 2 — and the compiler rewrites those into cell reads and writes for you.
component Counter() {
const &[count] = track(0)
<p>Count: {count}</p>
<button onClick={() => count++}>increment</button>
}
The binding is the API
Anything you can do to a variable, you can do to a tracked binding. There is no setter to call and no getter to remember.
| You write | It means |
|---|---|
| count | read — subscribes whatever expression it appears in |
| count++ | read and write |
| count = 5 | write |
| count === 2 | read — reactive condition |
| '/api/' + count | read — reactive string building |
The binding is deep. Once a tracked value holds an object or an array, mutating through it is a write — you do not have to reassign the whole thing.
component User() {
const &[user] = track({ name: 'ada', hits: 0 })
const &[tags] = track<string[]>([])
const visit = () => {
user.hits++
tags.push('visit')
}
<button onClick={visit}>{user.name} — {user.hits} hits, {tags.length} tags</button>
}
When you need the raw cell
The second name in the pattern is the raw cell — an object, not a value. Ask for it when you are handing the CELL to something that cannot see a binding. Skip it otherwise: if you are only reading or writing, the binding is shorter and clearer.
component Counter() {
const &[count, countCell] = track(0)
const bump = (cell: { get(): number; set(v: number): void }) => cell.set(cell.get() + 1)
bump(countCell)
<p>{count}</p>
}
- An API that takes a cell — useFetch({ into: cell }), createResource({ into: cell }). This is the main reason the raw cell exists.
- A helper that receives state as an argument — pass the cell, not the value, so the helper reads the CURRENT value instead of a snapshot taken when it was defined.
- Storing state in a structure — an array of setters, a map of cells, a callback queue. Each entry has to stay live rather than freeze at the value it held.
Passing the VALUE to a helper is the bug this prevents. const show = () => alert(count) captures count once; const show = () => alert(countCell.get()) always reads the current value.
The second name is NOT a callable setter. setCount(5) throws — there is no setCount. Write count = 5, or countCell.set(5).
Initialising from props
component Counter(props: { start: number }) {
const &[count] = track(props.start)
<p>Starting at {count}</p>
}
Many cells in one component
Cells are independent. Writing one never touches another, and only the expressions that read that particular cell re-run.
component SignupForm() {
const &[name] = track('')
const &[agree] = track(false)
<label>
<input value={name} onInput={(e) => name = e.target.value} />
</label>
<label>
<input type="checkbox" checked={agree} onChange={() => agree = !agree} />
</label>
<button disabled={!agree || name === ''}>Continue</button>
}
A cell can hold anything
component Shop() {
const &[cart] = track<string[]>([])
const add = (sku: string) => cart.push(sku)
<button onClick={() => add('vesk-ts')}>Add</button>
<p>{cart.length} in cart</p>
}
When to reach for something else
- A value that should be computed, not stored: derived.
- Work that should happen when a value changes: effect.
- A value derived from props that never changes at runtime: a plain const is fine, and cheaper.
- Reading state without subscribing: peek / untrack — see get and set.