State Hoisting in Jetpack Compose: Reducing Unnecessary Recompositions
Learn how hoisting state with remember and mutableStateOf keeps composables stateless, limits recomposition scope, and improves UI performance.
31 Aug 2026, 07:15 UTC

The problem: wasteful recompositions
When a composable holds its own state, any change to that state triggers a recomposition of the entire subtree. In a UI with many nested components, this can cause extra work that does not affect the visual output, slowing down frame rendering and wasting battery.
Thesis: hoist state to make composables stateless
State hoisting moves the source of state up to a common ancestor, leaving child composables as pure functions of their parameters. By using remember and mutableStateOf, the hoisted state survives recompositions but does not cause unrelated parts of the tree to recompose.
Worked example: a counter button
Consider a button that increments a number and displays the current count. Without hoisting, the button composable owns the state:
@Composable
fun CounterButton() {
var count by remember { mutableStateOf(0) }
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) { Text("Increment") }
}
}
Every click causes the whole CounterButton to recompose, even though only the Text needs to update.
Hoisting the state to a parent makes the button stateless:
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
CounterUI(count = count, onIncrement = { count++ })
}
@Composable
fun CounterUI(count: Int, onIncrement: () -> Unit) {
Column {
Text(text = "Count: $count")
Button(onClick = onIncrement) { Text("Increment") }
}
}
Now CounterUI only reads count and onIncrement. When count changes, Compose recomposes only the Text that reads it; the button itself does not recompose because its parameters have not changed.
Trade‑off and limitation
Hoisting can propagate unnecessary recompositions if the parent recomposes for reasons unrelated to the hoisted state. To avoid this, keep the hoisted state as granular as possible—hoist only the specific values a child needs, not a large data object that changes frequently for other reasons.
Another limitation is that remember does not survive configuration changes (e.g., screen rotation) unless paired with rememberSaveable or a ViewModel. If state must persist across such events, move it to a ViewModel and collect it with collectAsState.
How to verify the effect
- Create a new Compose project in Android Studio (Empty Compose Activity).
- Replace the generated
setContentwith either the non‑hoisted or hoisted version above. - Run the app on an emulator or physical device.
- Open Layout Inspector (View → Tool Windows → Layout Inspector).
- Select the
CounterUIcomposable (orCounterButtonfor the non‑hoisted version). - Observe the Recomposition count field while tapping the button.
- You should see the count increase only for the
Textnode in the hoisted version, whereas the non‑hoisted version shows recompositions for the entire subtree.
This comparison makes the recomposition scope visible without needing to instrument code.
Actionable closing
Start by identifying composables that own mutable state but only pass that state down to a few children. Hoist that state to the lowest common ancestor, keep the children as pure functions, and verify the impact with Layout Inspector. Remember to pair remember with rememberSaveable or a ViewModel when configuration‑change safety is required. Applying this pattern consistently reduces wasted work and makes your Compose UI easier to test and reason about.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.