Stop Recomposing Your Whole List on Every Keystroke with derivedStateOf
Use derivedStateOf to memoize filtered lists and UI flags in Jetpack Compose so recomposition only happens when the projected result actually changes, not on every source update.
12 Nov 2025, 02:54 UTC

The problem is noisy source state, not a slow list
A search field updates on every character. The items list may be large and the filter logic is cheap, but recomposing the entire LazyColumn for each keystroke is wasteful. The useful takeaway is to separate the fast-changing source state from the UI projection and only emit when the projection actually changes.
Jetpack Compose recomposes any composable that reads observable state. If the filter is computed directly in composition, every change to query or items triggers recomposition of the list and its children, even when the resulting set of visible items is identical.
What derivedStateOf actually memoizes
derivedStateOf is a Compose runtime API that tracks reads of Compose state inside its block and memoizes the result. It only emits a new value when the derived result is not equal to the previous one. Downstream composables that read the derived state therefore skip recomposition when the projection is stable.
It is designed for cases where source state changes frequently but the UI projection changes infrequently: filtered lists, search results, scroll-based flags, aggregated metrics.
The pattern that works well is to hoist source state out of composables, create the derived projection in a stable holder with remember, and consume the derived value in a narrow composition scope.
Practical pattern: filtered list with hoisted state
Keep query and items in a ViewModel or other stable holder. Build the filtered list with derivedStateOf so LazyColumn only reacts when the filtered set changes.
// ViewModel or state holder\nclass SearchHolder {\n var query by mutableStateOf(\"\")\n var items by mutableStateOf(listOf())\n}\n\n// Composable\n@Composable\nfun SearchScreen(holder: SearchHolder) {\n val query = holder.query\n val items = holder.items\n\n val filtered by remember {\n derivedStateOf {\n if (query.isBlank()) items\n else items.filter { it.title.contains(query, ignoreCase = true) }\n }\n }\n\n Column {\n TextField(\n value = query,\n onValueChange = { holder.query = it },\n label = { Text(\"Search\") }\n )\n LazyColumn {\n items(filtered, key = { it.id }) { item ->\n ItemRow(item)\n }\n }\n }\n}\nHere LazyColumn observes filtered, not query and items directly. Recomposition of ItemRow is limited to changes in the filtered list identity, not to every keystroke.
Key points for stability: the block reads only Compose state, items is a stable List type, and the derived value is consumed with a single read. Capturing large objects or unstable data classes inside the block can cause unnecessary invalidations.
Costs and when not to use it
derivedStateOf adds tracking overhead and an allocation for the derived state object. For cheap calculations or when the source changes infrequently, the overhead can outweigh the benefit.
It does not replace proper state hoisting. Keeping the derived computation inside a composable that is recreated often still leaks recomposition scope.
Behavior and stability guarantees are version sensitive across Compose compiler and runtime releases. Confirm availability and semantics against your project’s Compose BOM in build files.
Check the result in your project
Verify your Compose BOM version in the project root build file. Example check from terminal:
grep -n \"compose-bom\" app/build.gradleRun the app with Android Studio recomposition counts enabled and compare recomposition frequency before and after introducing derivedStateOf. A minimal screen with observable source state and a derived projection can be used to confirm emission only on actual result change.
Use Compose compiler reports to identify unstable types that may cause extra invalidations. If the derived block reads non-state values or unstable data, refactor to make inputs stable or move the logic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.