100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Kotlin

State Hoisting

Learn the Compose pattern of moving state up to a caller so composables become stateless, reusable, and easier to test.

State ManagementIntermediate9 min readJul 8, 2026
Analogies

State Hoisting

State hoisting is the practice of moving state out of a composable and into its caller, replacing the composable's internal var with two parameters: a value and an onValueChange (or similarly named) callback. A composable written this way is called 'stateless' — not because it has no relationship to state, but because it does not own or manage that state itself. Instead, the owner of the state (often a parent composable or a ViewModel) controls it, and the child composable simply reflects whatever value it's given and reports user-triggered changes back upward. This single pattern, borrowed from unidirectional data flow architectures used broadly in UI programming, is what makes Compose UIs composable in the truest sense: the same UI piece can be reused with different data sources without modification.

🏏

Cricket analogy: State hoisting is like a bowler not deciding their own field placement, the captain (owner of state) sets the field and the bowler just bowls to it and signals back if a change is needed, making the same bowler reusable under any captain's tactics.

From Stateful to Stateless

Consider a counter widget. A naive, stateful version keeps its own remember { mutableStateOf(0) } internally — it works, but it can only ever be driven by its own clicks; no external code can read or reset its value, and it can't be tested without simulating clicks. Hoisting the count out turns the composable into a pure function of its inputs: count: Int and onIncrement: () -> Unit. Now a parent can own the actual state, persist it, share it with sibling composables, or drive it from a ViewModel, all without touching the counter composable's code.

🏏

Cricket analogy: A naive stateful counter is like a scorer who only updates the board from their own personal count with no way for the umpire to correct or reset it; hoisting turns it into a pure display of runs and wickets fed from the official record, testable by simulating any scenario.

kotlin
// Stateful — owns its own state, hard to reuse or test
@Composable
fun StatefulCounter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) { Text("Count: $count") }
}

// Hoisted — stateless, reusable, easy to test
@Composable
fun StatelessCounter(
    count: Int,
    onIncrement: () -> Unit
) {
    Button(onClick = onIncrement) { Text("Count: $count") }
}

@Composable
fun CounterScreen() {
    var count by rememberSaveable { mutableStateOf(0) }
    StatelessCounter(count = count, onIncrement = { count++ })
}

Where Should State Actually Live?

Hoisting isn't 'hoist everything to the top'; the rule of thumb is to hoist state to the lowest common ancestor of all composables that need to read or modify it — no higher, no lower. State needed by a single composable and nowhere else can stay local with remember. State shared by siblings should live in their common parent. State that needs to survive configuration changes, come from a repository, or be shared across whole screens typically belongs in a ViewModel, with the Compose tree observing it via collectAsStateWithLifecycle or similar.

🏏

Cricket analogy: Hoisting to the lowest common ancestor is like keeping a bowler's personal run-up rhythm private to that bowler, sharing field placement decisions at the team-captain level, and keeping the tournament's overall standings at the board level, each piece of state at its right altitude.

State hoisting mirrors the classic 'props down, events up' pattern found in many declarative UI frameworks — Compose didn't invent the idea, but it makes the pattern nearly unavoidable because composables are just functions, and functions are naturally easiest to reuse and test when they're pure.

Over-hoisting — pushing every scrap of state, including truly local UI state like whether a tooltip is currently showing, all the way up to a screen-level ViewModel — adds unnecessary parameters, callback plumbing, and coupling. Not all state needs to be hoisted; only hoist as far as an actual consumer requires.

  • State hoisting replaces an internal var with a value parameter and an onValueChange-style callback, making the composable stateless.
  • Stateless composables are more reusable and far easier to unit test since they're pure functions of their parameters.
  • The rule of thumb is to hoist state to the lowest common ancestor that needs it — not necessarily all the way to a ViewModel.
  • Sibling composables that need to share state should have that state hoisted to their common parent.
  • Screen-level or persistence-requiring state typically lives in a ViewModel and flows down via State/StateFlow.
  • Over-hoisting truly local state adds unnecessary complexity; hoist only as far as consumers require.

Practice what you learned

Was this page helpful?

Topics covered

#Kotlin#AndroidWithJetpackComposeStudyNotes#MobileDevelopment#StateHoisting#State#Hoisting#Stateful#Stateless#StudyNotes#SkillVeris