Ask
67
@couchto5kagain ·

new to compose: is hoisting state until eleven lambdas reach the leaf normal swiftui-state

Coming from XML layouts and view binding. I have read the state hoisting guidance and I think I have followed it, but the result feels wrong and I cannot tell if that is because it is wrong or because it is unfamiliar.

My settings screen holds all the state. To get a toggle down to a row inside a section inside the screen, I pass a lambda. There are eleven of them now, and the middle composables do not use any of them - they exist purely to forward parameters down. A signature is now four lines long.

Is this what everyone's code looks like and I should stop worrying, or have I taken the advice too literally?

8 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @two_calendars · 9mo ago · 3 replies

    Taken too literally. The guidance says hoist to the lowest common ancestor of everything that reads or writes the state, and people read it as "hoist to the top".

    If a toggle's value is only read by one row, its state belongs in that row. It gets hoisted when something else needs it - a save button that must know the value, a validation message, whatever. Not before.

    For the ones that genuinely do belong at the screen level, the standard shape is a state object and a single event channel:

    data class SettingsUiState(
        val notificationsEnabled: Boolean,
        val theme: Theme,
        val syncOverCellular: Boolean,
    )
    
    sealed interface SettingsEvent {
        data class ToggleNotifications(val on: Boolean) : SettingsEvent
        data class SetTheme(val theme: Theme) : SettingsEvent
    }
    
    @Composable
    fun SettingsSection(
        state: SettingsUiState,
        onEvent: (SettingsEvent) -> Unit,
    )
    

    Two parameters instead of eleven. Adding a new setting changes the data class and the sealed interface, not every signature between the screen and the row.

    The cost is that onEvent is less explicit than a named callback, so a leaf can now emit any event. In practice that has never caused me a problem, and eleven-parameter signatures caused me problems weekly.

    So: your instinct is right. It is a real smell, and it is the most common way people over-apply the pattern in their first months.

    61
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @couchto5kagain · 9mo ago

      The lowest common ancestor bit is what I was missing. I had read hoisting as a direction rather than as a destination.

      21
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @durable_ines · 9mo ago

      One caveat on the onEvent shape: keep it per-screen rather than one app-wide event type. A single giant sealed interface for the whole app makes every leaf theoretically able to trigger anything, and that does eventually hurt.

      17
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @whetstone_wu · 9mo ago · 2 replies

    The thing you have discovered has a name outside Compose - prop drilling - and every declarative UI framework rediscovers it.

    The escape hatch is CompositionLocal, and the reason nobody recommends it immediately is that it is easy to misuse. Good uses are genuinely ambient: theme, typography, a haptics controller, the current window size class. Things that a hundred composables might want and no one composable owns.

    Bad use is app state. Once your settings values come from an ambient source, a composable's output no longer depends only on its parameters, previews break, and you cannot tell what a component needs by reading its signature. You have traded an ugly signature for an invisible dependency, which is worse.

    Rule I use: if a reasonable person would want to see it in the signature, it goes in the signature.

    44
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @muslin_mira · 9mo ago

      The SwiftUI equivalent is @Environment and the exact same rule applies, including the part where everyone puts their whole app state in it once and then spends a year untangling it.

      19
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report