Choosing Between remember and derivedStateOf in Jetpack Compose
Use remember with keys for cheap, eagerly computed derivations. Use derivedStateOf when the value depends on State objects and you want calculation deferred until the UI actually reads it—avoiding work during compositions where the derived value isn't needed.
08 Nov 2025, 17:50 UTC

The Short Answer
Use remember with keys for values computed once per composition when inputs change. Use derivedStateOf when the derived value depends on other State objects and you want to defer calculation until the value is actually read during composition, avoiding unnecessary recompositions of downstream composables.
How Each Mechanism Works
remember with Keys
remember(key1, key2) { compute() } caches the result of compute() and re-executes it only when any key changes. The Compose compiler may inline the call, so there's no extra object allocation for trivial derivations. Keys are compared using equals(), so primitive types and stable data classes work well.
derivedStateOf
derivedStateOf { ... } creates a DerivedState record that participates in the snapshot system. When a composable reads the derived value during composition, Compose registers a dependency on the snapshot state objects accessed inside the lambda (e.g., MutableState, State). The lambda re-runs only when one of those dependencies changes and the derived value is read again. This defers work until the UI actually needs the result.
Worked Example: Filtering a List
Suppose you have a list of items and a search query, both held in MutableState. You want to display a filtered list without re-filtering on every recomposition of the parent.
@Composable
fun SearchableList(
items: MutableState<List<Item>>,
query: MutableState<String>
) {
// derivedStateOf: filtering runs only when query or items change,
// and only if filteredList is read during composition
val filteredList = derivedStateOf {
items.value.filter { it.name.contains(query.value, ignoreCase = true) }
}
Column {
Text(text = "Results: ${filteredList.value.size}")
LazyColumn {
items(filteredList.value) { item ->
Text(text = item.name)
}
}
}
}
Here, filteredList is a State<List<Item>>. The filter lambda executes only when items.value or query.value changes and a composable reads filteredList.value. If the parent recomposes but neither dependency changed, the lambda is skipped entirely.
Equivalent with remember
@Composable
fun SearchableListRemember(
items: MutableState<List<Item>>,
query: MutableState<String>
) {
val filteredList = remember(items.value, query.value) {
items.value.filter { it.name.contains(query.value, ignoreCase = true) }
}
// ... same UI
}
This works, but the filter runs during every composition where items.value or query.value differs from the previous composition, even if no child composable reads filteredList. With derivedStateOf, the calculation is deferred until the read site.
When to Prefer Each
| Scenario | Recommended | Reason |
|---|---|---|
| Cheap computation, keys change infrequently | remember(key1, key2) | No snapshot overhead; compiler may inline |
Derived value read conditionally (e.g., inside a LazyColumn that may not compose all items) | derivedStateOf | Calculation skipped if value never read |
| Multiple composables read the same derived value | derivedStateOf | Single calculation shared across readers |
Inputs are plain variables, not State objects | remember with keys | derivedStateOf only tracks snapshot state reads |
Trivial derivation (e.g., a + b, string concatenation) | remember | derivedStateOf always allocates a record |
Common Mistakes
1. Wrapping Simple Calculations in derivedStateOf
// Unnecessary overhead
val sum = derivedStateOf { a.value + b.value }
// Better
val sum = remember(a.value, b.value) { a.value + b.value }
The derivedStateOf version allocates a DerivedState record and participates in snapshot tracking for a calculation that costs nanoseconds. The remember version is inlined and effectively free.
2. Using remember Without Keys When Inputs Are State Objects
// Stale value risk
val filtered = remember { items.value.filter { it.matches(query.value) } }
Without keys, the lambda runs only once during initial composition. Changes to items.value or query.value won't trigger re-execution. Always pass the relevant .value properties as keys, or use derivedStateOf which tracks them automatically.
3. Capturing Composable-Local Variables Without Keys
@Composable
fun BadExample() {
val config = remember { loadConfig() }
val derived = derivedStateOf { compute(config.value) } // config not tracked!
}
derivedStateOf only tracks snapshot state reads (MutableState, State). The config variable here is a plain object reference; changes to config.value won't invalidate the derivation. Either make config a MutableState or use remember(config.value) { ... }.
4. Using derivedStateOf for Side Effects
// Wrong: side effect inside derivedStateOf
val _ = derivedStateOf {
analytics.logEvent("filter_changed", query.value)
query.value.filter { ... }
}
derivedStateOf lambdas must be pure calculations. They can run multiple times per frame, during snapshot apply, or not at all if unread. Use LaunchedEffect(query.value) { ... } or DisposableEffect for side effects.
Limits and Trade-offs
derivedStateOf Does Not Prevent Parent Recomposition
Reading a derivedStateOf value inside a composable still subscribes that composable to recomposition when the derived value changes. If the parent composable reads it, the parent recomposes. To isolate recomposition, lift the derived state higher or hoist the reading composable into a separate function that only recomposes when the derived value changes.
Memory Overhead
Each derivedStateOf creates a DerivedState record retained for the composition's lifetime. In a list with thousands of items, creating a derived state per item adds measurable memory pressure. Profile with composeCompilerReport and the Layout Inspector if you suspect overhead.
Compiler Inlining
The Compose compiler inlines remember calls when the lambda is simple and keys are stable. derivedStateOf is never inlined—it always allocates an object. For trivial derivations, this difference matters in tight loops or frequently recomposing scopes.
Verification Checklist
- Log recompositions: Add a
SideEffect { log("recomposed") }in the composable reading the derived value. Compare counts betweenrememberandderivedStateOfversions. - Check compiler report: Run
./gradlew composeCompilerReportand verify your state-holding classes are markedstableorimmutable. Unstable classes force recomposition regardless ofderivedStateOf. - Inspect snapshot reads: In Android Studio's Layout Inspector, enable "Show recomposition counts" to see which composables recompose when the query changes.
- Profile memory: Use the Profiler's heap dump to count
DerivedStateinstances if you create many in a list.
Quick Decision Flow
- Is the computation trivial (single expression, no loops)? →
remember(keys) - Do inputs come from
MutableState/Stateand is the result read conditionally or by multiple composables? →derivedStateOf - Are inputs plain variables or non-state objects? →
remember(keys)with explicit keys - Do you need a side effect when inputs change? →
LaunchedEffect(keys), notderivedStateOf
Start with remember(key1, key2). Switch to derivedStateOf only when profiling shows the derivation runs too often or when you need conditional read semantics. The snapshot overhead is real, and the compiler cannot optimize it away.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.