Implementing State Hoisting in Jetpack Compose for Reusable UI
Learn how to implement state hoisting in Jetpack Compose to decouple UI from logic, improve testability, and create reusable stateless components.
28 May 2026, 05:37 UTC

The Problem: Rigid UI Components
When a Composable manages its own internal state using remember { mutableStateOf(...) }, it becomes a "stateful" component. While convenient for simple prototypes, stateful components are difficult to test because you cannot force them into a specific state from the outside. They are also hard to reuse; if another part of your app needs the same UI but different data logic, you must rewrite the component.
The Takeaway: State hoisting transforms a stateful composable into a stateless one by moving the state to the caller. This creates a unidirectional data flow: the parent passes data down (state) and the child passes events up (callbacks).
Prerequisites
- Android Studio Flamingo or newer.
- Jetpack Compose BOM 2023.05.00 or later.
- Basic familiarity with
MutableStateandremember.
Procedure: Converting Stateful to Stateless
Consider a simple search field. A stateful version would hold the text internally. To hoist this state, follow these steps:
1. Define the Stateless Composable
Remove the remember block from the child. Instead, define two parameters: the current value and a lambda function to handle changes.
// Stateless Composable
@Composable
fun SearchInput(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = query,
onValueChange = onQueryChange,
label = { Text("Search") },
modifier = modifier
)
}
2. Create the Stateful Wrapper
Create a parent composable that manages the state. This is where you decide how the state is stored—whether in a ViewModel for business logic or using rememberSaveable to survive configuration changes like screen rotations.
// Stateful Wrapper
@Composable
fun SearchScreen() {
// rememberSaveable persists state across activity recreation
var searchQuery by rememberSaveable { mutableStateOf("") }
Column {
SearchInput(
query = searchQuery,
onQueryChange = { newValue -> searchQuery = newValue }
)
Text("Current search: $searchQuery")
}
}
Comparison: Stateful vs. Stateless
| Feature | Stateful Composable | Stateless (Hoisted) Composable |
|---|---|---|
| Ownership | Owns its own state | Owned by the caller |
| Testability | Hard (requires UI interaction) | Easy (pass specific values) |
| Reusability | Low (tied to internal logic) | High (purely presentational) |
| Control | Internal only | External (Parent controls value) |
Verification and Testing
To verify that state hoisting is implemented correctly, perform these three checks:
- Preview Validation: Create a
@Previewfor the statelessSearchInput. Pass a hardcoded string (e.g., "Hello World"). If the UI renders that specific text without needing a ViewModel, the component is successfully stateless. - State Flow Check: In the
SearchScreen, change theonQueryChangelambda to{ newValue -> searchQuery = newValue.uppercase() }. If the text in theSearchInputautomatically converts to uppercase, the unidirectional data flow is working. - Configuration Test: Run the app on an emulator and rotate the screen. If the text remains in the field,
rememberSaveableis correctly hoisting the state at the wrapper level.
Engineering Limitations and Risks
While hoisting is powerful, it introduces two primary risks:
- Prop Drilling: If you hoist state too high, you may find yourself passing the same state and callback through five layers of components that don't actually use the data, just to reach a child that does. In these cases, consider using a
ViewModelor a CompositionLocal for deeply nested dependencies. - Recomposition Overload: If the hoisted state is a complex object that changes frequently, the entire parent and all its children may recompose. Ensure that state objects are stable or use
derivedStateOfto limit updates to only the components that need them.
Rollback Strategy
If hoisting creates excessive complexity (prop drilling), you can revert the component to a stateful pattern by moving the mutableStateOf declaration back inside the child composable and removing the state parameters from the function signature.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.