State & reactivity
Kinetica has one reactive primitive: the cell. state, derived and store are all cells (resources cache their values in the same machinery); reads inside render are tracked, a write invalidates the render, and memoization prunes the re-render down to the components whose inputs actually changed. Contexts are the one ambient mechanism that is not a cell — they are scoped values, not reactive state.
state
var count by state { 0 }
count += 1 // write -> invalidate -> re-renderstate allocates a slot in the component's frame. Slot identity is assigned by the compiler plugin: every call site gets a static ordinal, and the value lives in a per-instance array indexed by it. There are no keys to pass and no positional collisions — two branches of an if/else are two different call sites, so they can never share a slot. Writes go through an EqualityPolicy — structural() by default — so writing an equal value does not re-render.
Dynamic identity (the same call site rendering many logical instances) comes from the keyed constructs: each(items, key = { … }) gives every row its own frame, and keyed(key) { … } does the same for a single subtree. When a key disappears from the list its frame is disposed — state does not resurrect if the key later returns.
fun <T> ComponentScope.state(
policy: EqualityPolicy<T> = EqualityPolicy.structural(),
persistent: Boolean = false, // slot survives keyed-frame disposal, see /docs/persist
transient: Boolean = false, // slot is dropped when not touched by a render
initial: () -> T,
): MutableCell<T>Signatures on these pages omit the ordinal: Int = -1 parameter the compiler plugin fills in at every slot-consuming call site — you never pass it yourself. A second overload, state(slotId = …, persistent = true) { … }, addresses a slot by a stable SlotId for persistence. The returned MutableCell also has update { it + 1 } for read-modify-write.
derived
val remaining by derived { todos.count { !it.done } }derived memoizes a computation and re-runs it only when a tracked dependency's version changes. Equality-deduped: if the recomputed value is equal, downstream readers do not re-render — derived { count > 0 } re-renders its readers only when the boolean flips.
store — cells outside the render tree
val theme = store(Theme.Light) // plain reactive cell, no component scope needed
val current = peek { theme.value } // read without dependency trackingstore builds reactive objects that live outside components (the forms battery uses it for field state). Renders that read a store re-render when it changes.
Events
val add = event { todos = todos + draft } // () -> Unit
val onInput = event<String> { draft = it } // (String) -> Unitevent {} returns a handler with a stable identity across renders (slot-backed), so a re-render does not re-register anything. Handlers run on the single UI loop: the event executes, all cell writes apply, then exactly one synchronous render commits. There is no batching to configure and no torn intermediate state to observe.
Propagation is glitch-free: each write recomputes an affected derived at most once and fires observers only after the whole graph has settled, so an observer never reads a half-updated mix of old and new values. This is a single-writer contract — writing cells concurrently from several threads into the same observed graph, or writing a cell from inside one of its observers, is outside it. There is also no update-depth guard: an effect that unconditionally writes state it also watches re-renders forever.
Try it
Live — this widget is a Kinetica app mounted in your browser.
The counter above is state + derived + two event handlers, mounted with the browser renderer. So is this input mirror:
Live — this widget is a Kinetica app mounted in your browser.
Equality policies
| Policy | Behavior |
|---|---|
structural() | == comparison (default) |
referential() | === comparison |
neverEqual() | every write invalidates |
EqualityPolicy is a fun interface — pass EqualityPolicy { a, b -> … } for custom change detection.
Rules of thumb
- Model state as immutable values (
data classcopies, new lists). The renderer diffs values — mutation in place defeats change detection. - Read cells where you render them; derive aggregates with
derivedinstead of recomputing in every component. - Effects never run during render — reach for
watchandlaunchEffectwhen something must happen because state changed.