State hoisting is the pattern Compose uses instead of two-way data binding: rather than a component owning and mutating its own state, you move (“hoist”) that state up to a caller and pass it down as parameters, along with a callback for changing it. It’s the single most important pattern for writing reusable, testable Compose UI.
The Problem: A Composable That Owns Its Own State
@Composable
fun SearchField() {
var query by remember { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}
This works, but nothing outside SearchField can read the current query, clear it, or set it from elsewhere. You can’t filter a list based on it, and you can’t write a meaningful test for it without rendering the whole composable.
The Fix: Hoist the State
@Composable
fun SearchField(
query: String,
onQueryChange: (String) -> Unit
) {
TextField(value = query, onValueChange = onQueryChange)
}
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
Column {
SearchField(query = query, onQueryChange = { query = it })
Text("You searched for: $query")
}
}
SearchField no longer holds any state of its own — it just displays whatever query it’s given and reports changes upward via onQueryChange. This is called a stateless composable, and SearchScreen, which owns the remember block, is the stateful one.
Why This Matters
- Reusability:
SearchFieldcan now be used anywhere, wired up to any source of truth — a localremember, aViewModel, or a value derived from something else entirely. - Single source of truth: the state lives in exactly one place, so there’s no risk of the UI and the underlying data drifting out of sync.
- Testability: you can render
SearchFieldin a test with a fixedqueryand a fakeonQueryChange, with no need to simulate remember or recomposition.
How Far Up Should You Hoist?
Only as far as the state actually needs to go. A common mistake is hoisting everything all the way to a ViewModel, even for state that’s purely a UI concern and never used outside one screen — for example, whether a password field is currently obscured. That kind of state is fine living in remember at the screen level; it doesn’t need to survive a rotation or be visible to business logic.
The general rule: hoist state to the lowest common ancestor of every composable that needs to read or write it — no further.