State Hoisting: State Lifting and Unidirectional Data Flow in Compose

Author: IT Sectr Published: 2026-06-28 Reading time: 8 min

State Hoisting is a pattern in Jetpack Compose where state is moved out of a child Composable function into a parent, and the child receives data through parameters and notifies about changes via callbacks. This is an implementation of the Unidirectional Data Flow (UDF) principle, where state flows up and events flow down. According to Google Android Developers, 2026, State Hoisting makes components reusable, testable, and predictable.

Key Takeaways

  • State Hoisting moves state from a child component to a parent
  • UDF (Unidirectional Data Flow) — state flows down, events flow up
  • Parameters of a child component: value (T) + lambda (T) -> Unit
  • Reusability — hoisted state allows using the same function with different data sources
  • Testing — State Hoisting simplifies unit tests by isolating logic from UI

What is State Hoisting in Jetpack Compose

State Hoisting is a pattern where a Composable function does not own state but receives it from outside. Instead of var inside the function, two parameters are used: a value for display and a lambda callback for handling changes. Technically, this means the child component becomes stateless (does not have its own state), while the parent is stateful (owns the state).

Example: the TextField component from Material3 does not store the entered text internally. It accepts value: String and onValueChange: (String) -> Unit. The parent calling TextField declares var value by remember { mutableStateOf("") } and passes value and onValueChange. This is classic State Hoisting: TextField is a dumb component (simply displays and reports input), the parent is smart (owns the state).

Stateless vs Stateful: A stateless component is easier to test — it does not depend on internal state, its behavior is fully determined by input parameters. A stateful component is convenient for rapid prototyping but harder to reuse: it is tightly coupled to a single data source. State Hoisting gives you a choice: any component can be made stateless by moving state upward.

Unidirectional Data Flow (UDF) and State Hoisting

UDF (Unidirectional Data Flow) is an architectural principle where data moves in one direction: from the source of truth (ViewModel or parent Composable) to the UI, and events flow in the opposite direction. State Hoisting is the implementation of UDF at the individual component level. Instead of each component deciding when and how to change its own state, it notifies the parent about an event, and the parent decides how to change the state.

Advantages of UDF: predictability — state changes in only one place, eliminating race conditions; traceability — the call stack allows reconstructing the chain of changes; testing — stateful logic can be extracted into a separate class and tested without UI. In large projects, UDF combined with State Hoisting is the de facto standard.

Single Source of Truth is another principle that accompanies UDF. Each piece of state has exactly one source. If two components use the same state, the source should be shared (at the ViewModel or common parent level). State Hoisting ensures the source is above in the hierarchy and no state duplication occurs.

DirectionWhat is passedHow it is implemented
Down (parent → child)Value to displayParameter value: T
Up (child → parent)Change eventParameter onValueChange: (T) -> Unit

State Hoisting Rules: When and How to Lift State

The main rule: state should be lifted to the minimum possible level sufficient for all components that need it. If state is only used inside one component — keep it local. If two adjacent components need the same state — lift it to the common parent. If state is needed across the entire screen — lift it to the ViewModel.

The minimum lift rule prevents unnecessary complexity. There is no point in lifting text field state to a ViewModel if it is only used within one screen and is not persisted when the Activity is recreated. Use rememberSaveable at the screen parent level, not ViewModel, for UI state that should survive a screen rotation but is not needed by business logic.

When to lift to ViewModel: if the state must survive Activity recreation, if it is needed by multiple screens, if changing the state triggers business logic (network requests, database). State Hoisting at the ViewModel level is a standard pattern in MVVM architecture, where the UI layer is stateless and the ViewModel is stateful.

kotlin
    // ❌ Bad: component owns its own state
@Composable
fun BadTextField(label: String) {
    var text by remember { mutableStateOf("") }
    TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}

    // ✅ Good: State Hoisting — state in parent
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
    TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}

    // Usage: parent owns the state
@Composable
fun Form() {
    var name by rememberSaveable { mutableStateOf("") }
    GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}

State Hoisting Examples in Real Components

Consider a login screen with two fields (email, password) and a button. All three components receive state through State Hoisting: email and password are managed by the parent, the button receives its enabled status as a value.

kotlin
    // State Hoisting at screen level
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    Column(modifier = Modifier.padding(16.dp)) {
        // Email field — State Hoisting via lambda
        EmailField(
            email = uiState.email,
            onEmailChange = { viewModel.onEmailChanged(it) }
        )

        // Password field — similarly
        PasswordField(
            password = uiState.password,
            onPasswordChange = { viewModel.onPasswordChanged(it) }
        )

        // Button — receives only enabled (read-only)
        LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
    }
}

// Stateless component: receives email + callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
    OutlinedTextField(
        value = email,
        onValueChange = onEmailChange,
        label = { Text("Email") },
        singleLine = true
    )
}

// Stateless button component
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
    Button(onClick = onClick, enabled = enabled) {
        Text("Login")
    }
}

EmailField and PasswordField are completely stateless. They can be reused on any screen by connecting them to any data source. LoginButton receives enabled as read-only — this is another form of State Hoisting where state is not lifted (the button cannot enable itself) but is passed down ready-made. This approach provides maximum flexibility with minimal component coupling.

State Hoisting vs Local State: Selection Criteria

Not every state needs to be lifted. Local state (State inside a Composable) is justified when: data is only needed inside one component, it does not affect sibling elements, and it should not survive recomposition of a specific section. For example, animation state, input field focus, current scroll position — these are reasonable to keep locally.

When State Hoisting is necessary: state is used by multiple child components; a change in one child should be reflected in another; the logic of state changes needs to be tested separately from the UI; state should survive Activity recreation. In these cases, local state creates duplication and data inconsistency.

Hybrid approach: keep minimal state locally, lift the rest. The Compose rule: "lift state as high as necessary and as low as possible." In practice, this means starting with local remember and only lifting the level when access from another component is needed. Do not apply State Hoisting preemptively — it complicates code without necessity.

Frequently Asked Questions

How is State Hoisting different from ViewModel?

State Hoisting is a UI component-level pattern. ViewModel is an architectural layer for business logic. State Hoisting can lift state to the parent Composable level, screen level, or ViewModel. ViewModel is the highest lifting point for state that must survive Activity recreation.

How to test a component with State Hoisting?

A stateless component is tested by simply passing values. Call the Composable with the required parameters and check the display using ComposeTestRule. State changes are tested at the parent or ViewModel level — separate from the UI. This significantly simplifies tests: there is no need to simulate recomposition inside the component.

Can State be lifted as read-only?

Yes, this is common practice. If a component only needs to display data without the ability to modify it — pass State<T> (read-only). The component will be subscribed to changes but cannot initiate them. This strengthens encapsulation and protects data from unwanted mutations.

What if state needs to be lifted 3+ levels deep?

For deep passing use CompositionLocal or pass through parent Composable parameters. If state is needed across the entire screen — extract it to a ViewModel and use collectAsState(). Passing through 5+ levels is a sign of incorrect architecture; reconsider the component hierarchy.

Does State Hoisting affect performance?

State Hoisting may slightly increase the number of recompositions since a change in the parent can recompose all children. Use derivedStateOf to filter changes and keys in LazyColumn for targeted updates. In most scenarios, the overhead of State Hoisting is negligible compared to the benefit of maintainability.

Summary

  • State Hoisting moves state to a parent component, making the child stateless
  • UDF ensures unidirectional data flow: state down, events up
  • Reusability — stateless components can be connected to any data source
  • Testing — UI tests only check display, logic is tested separately
  • Minimum lift — lift only as much as necessary
  • ViewModel — the highest lift point for state with business logic
  • Recommendation: start with local remember, lift only when needed

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also