← All courses

State Hoisting & Unidirectional Data Flow

🗓 May 31, 2026 ⏱ 2 min read

The problem with state inside a component

If a composable holds its own state, it can’t be controlled or tested from outside, and two copies can’t share data. The solution is state hoisting: move the state up to the caller, and pass the value down with an event (lambda) to change it.

Stateful vs stateless

// Stateless: gets its value and reports events — reusable & testable
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
    Button(onClick = onIncrement) {
        Text("Count: $count")
    }
}

// Stateful: owns the state, passes it down
@Composable
fun CounterScreen() {
    var count by rememberSaveable { mutableStateOf(0) }
    Counter(count = count, onIncrement = { count++ })
}

The reusable Counter knows nothing about where the count comes from. The same component could be driven by a ViewModel, a parent, or a test.

Unidirectional Data Flow (UDF)

This pattern has a name: state flows down, events flow up. The UI displays state; user actions send events upward to whoever owns the state, which updates it, which flows back down. It is predictable and easy to debug.

State owner UI (composable) state ↓ events ↑

Where should state live?

  • Keep it at the lowest common owner of everything that needs it.
  • If only one composable uses it, keep it there.
  • If it must survive navigation or rotation, or involves business logic, hoist it to a ViewModel.

Common mistakes

  • Hoisting state too high so everything recomposes unnecessarily.
  • Making components stateful when they should just receive a value and a callback.
  • Mutating shared state from deep inside the tree instead of sending an event up.
Summary: Hoist state to the caller and follow UDF — state flows down, events flow up. Stateless composables are reusable, testable and predictable.