State & Reactivity
$state declares state. $derived computes a value from it. $effect runs when it changes. A plain var or val is plain Kotlin.
Local state
A var declared with $state is state. Assign to it and whatever
reads it redraws:
var count = $state(0)
<Button onClick={() => count++}>
<Text>{count}</Text>
</Button> compiles to
var count by remember { mutableStateOf(0) } Reading count in markup subscribes to it, and assigning to it recomposes
whatever read it. You never write remember, mutableStateOf, or .value yourself.
A plain var is a Kotlin variable. Changing it redraws nothing, and if the
markup reads it anyway, that is a build error:
error[state-not-declared]: `count` is read by the markup but is not state; write `var count = $state(0)` Nothing is inferred from how a declaration is used. What it is written as is what it is.
Lists
$state([…]) declares a list changed in place. .add, .removeAt, and .clear each redraw what reads the list. It is
never reassigned, so it may be a val:
val todos = $state(["Buy milk"])
<Button onClick={() => todos.add("Ship")}>Add</Button> To replace a list whole, hold a list value instead and assign it:
var todos = $state(listOf("Buy milk"))
<Button onClick={() => todos = todos + "Ship"}>Add</Button> When state outlives the composable
A screen that only holds a couple of fields keeps them in rememberSaveable,
which survives rotation. A screen fed by a loader, one that reads $screen.params, or one holding a $state of a type a Bundle cannot carry is kept for the screen instead — on what
Android calls a ViewModel, generated for you. Nothing about the source changes either way; whitehall why <file> names where each $state is kept and why.
| Declaration | Survives rotation | Survives restart |
|---|---|---|
var x = $state(0), a Bundle-safe type | Yes | No |
var x = $state(User()) in a component | No | No |
var x = $state(User()) in a screen | Yes | No |
var x = 0 | Not state | No |
@store object X | Yes | No |
@store(persist) object X | Yes | Yes |
Derived values
$derived(expr) recomputes whenever anything expr reads changes:
var count = $state(0)
var multiplier = $state(2)
val doubled = $derived(count * 2) // tracks count
val scaled = $derived(count * multiplier) // tracks count and multiplier compiles to
val doubled by remember { derivedStateOf { count * 2 } } Dependencies are whatever the expression reads. There is no list to declare and no list to
keep in sync when you edit the expression. A $derived is always a val, and what reads it redraws only when its value changes.
For a computation that spans more than one expression, use the block form:
val label = $derived {
val n = items.size
if (n == 1) "1 item" else "$n items"
} The last expression in the block is the value.
A plain val is a Kotlin value, evaluated again on every redraw — so val total = items.sumOf { it.price } stays current without $derived. Reach for $derived for a computation worth not
repeating, or for a value computed once: val seed = $derived(Random.nextInt(30)) reads no state, so it runs once.
val x = expr runs on every read, and quietly changing that
for some declarations and not others would make the language guess. $derived says "this one is cached and tracked" out loud, so a plain val keeps meaning
exactly what it means in Kotlin.See it happen
The source is editable, and the Kotlin is compiled here in your browser by the same compiler the CLI uses — so it is what you would actually get, not a transcription of it.
import $lib.components.ui.Button
var count = $state(0)
<Column gap={16}>
<Text>Count: {count}</Text>
<Button onClick={() => count++}>Increment</Button>
</Column>$state(0) declares state. Reading count in markup subscribes to it; assigning to it redraws whatever read it.
Effects
$effect runs when its screen or component appears, and again whenever the
state it reads changes. It stops when the screen or component leaves:
var count = $state(0)
$effect {
println("Count changed to: $count")
analytics.track("count", count)
} compiles to
LaunchedEffect(Unit) {
snapshotFlow {
println("Count changed to: $count")
}.collect {}
} Use it for the things a value cannot express: logging, analytics, syncing to disk. If you
are computing something, reach for $derived instead — a derived value only
recomputes when read, while an effect runs whether anyone is looking or not.
A block that begins with val declarations reads its dependencies there, and
runs the rest as a coroutine, so it can call a suspend function:
$effect {
val q = query
results = search(q)
} $effect block makes it re-trigger
itself. Derive instead of assigning, or move the write behind an event handler.Collections
Bracket literals build lists, and mutability follows val versus var:
val numbers = [1, 2, 3] // listOf(1, 2, 3)
val todos = $state(["Buy milk"]) A $state([…]) list is state, so pushing to it recomposes the list that
renders it. See @for for iterating one.
Ranges
val simple = 1..10 // (1..10).toList()
val stepped = 0..100:2 // step 2
val down = 10..1:-1 // downTo Inside a function
In a function body, var n = $state(0) is mutableStateOf(0) and val d = $derived(n * 2) is derivedStateOf { n * 2 }.
Nothing is remembered: a function is not a screen.
Classes
class Cart {
var items = $state(listOf<Item>())
val total = $derived(items.sumOf { it.price })
fun add(item: Item) {
items = items + item
}
} A class with $state fields is a Kotlin class whose fields are state. Whoever
makes one holds it: val cart = Cart() in a component is remember { Cart() }, made once. A plain var field is a
plain field. The class's suspend functions stay suspend; a
handler that calls one launches it.
Explicit derivedStateOf
The Compose primitive is still available when you want it, which is worth knowing for an expression that is expensive enough to think about:
var items = $state(listOf<Item>())
val sorted = derivedStateOf { items.sortedBy { it.name } } Prefer $derived { … } for anything ordinary.
See Also
- @store — state that lives as long as the app
- Data classes — shaping the state you hold
- bind: — wiring state to inputs
- $onMount / $onDispose — running code on entry and exit