DisposableEffect, SideEffect, and rememberUpdatedState Compared

Quick answer: Use DisposableEffect to register something that must be unregistered, SideEffect to publish successfully composed state to non-Compose code, and rememberUpdatedState to let a long-lived effect read the newest value without restarting. They solve different lifecycle problems and are often used together.

Compose rendering may recompose, run in a different order, or be discarded, so do not perform outside work directly in a composable body. The official side-effects guide provides these APIs to make that work occur at a controlled point in composition.

Choose by lifecycle need

APIUse it whenKey behavior
DisposableEffectAn observer, listener, receiver, or resource requires cleanupDispose on key change or leaving composition
SideEffectA non-Compose object needs the latest successfully rendered valueRuns after every successful recomposition
rememberUpdatedStateA running effect needs the latest callback/value but must not restartUpdates a remembered State reference

None of them replaces durable UI state or a ViewModel. For work that launches a coroutine, see when to use LaunchedEffect.

DisposableEffect: pair setup with cleanup

Use it for a resource that must be reversed. This lifecycle observer registers when the displayed owner changes and removes itself when the owner changes or the composable leaves.

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner

@Composable
fun ScreenLifecycleReporter(
    lifecycleOwner: LifecycleOwner,
    onStart: () -> Unit,
    onStop: () -> Unit,
) {
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_START -> currentOnStart()
                Lifecycle.Event.ON_STOP -> currentOnStop()
                else -> Unit
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }
}

The current callbacks are deliberately not keys: changing them should update what the observer calls, not tear down and recreate the observer. A DisposableEffect must include a meaningful onDispose; an empty cleanup is a signal to choose a different API.

SideEffect: publish committed UI state

Use SideEffect when Compose owns a value but another object needs it after a composition successfully applies.

import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect

@Composable
fun UpdateAnalyticsUserType(
    userType: String,
    analytics: Analytics,
) {
    SideEffect {
        analytics.setUserProperty("user_type", userType)
    }
}

This is appropriate for synchronization that should happen after every successful composition. It is not suitable for a suspend request, a listener registration, or a one-time navigation command. Those have different lifetimes.

rememberUpdatedState: retain lifetime, refresh the value

It returns a state object whose value updates on recomposition while the object remains stable for the effect that captured it.

@Composable
fun SplashTimeout(onFinished: () -> Unit) {
    val currentOnFinished by rememberUpdatedState(onFinished)

    LaunchedEffect(Unit) {
        delay(2_000)
        currentOnFinished()
    }
}

Without it, including onFinished as a LaunchedEffect key would restart the delay whenever the parent supplies a new lambda. Only use this pattern when that non-restart behavior is intentional; otherwise make the value a key.

Common mistakes

  • Do not register a listener in SideEffect: it has no cleanup and runs after every successful recomposition.
  • Do not put mutable dependencies outside DisposableEffect keys when a new dependency should replace the old registration.
  • Do not use rememberUpdatedState to hide a dependency that must restart work.
  • Do not perform a state-changing effect in the composable body. Keep rendering side-effect free.

For state ownership and transient commands, read UI state, events, and one-time effects. For lifecycle-aware Flow state, use collectAsStateWithLifecycle.

Quick check

When a key changes, verify the old listener is removed before the new one is registered. Recompose with a new callback and verify whether the correct result is a restart or an updated callback. Finally, remove the composable and confirm every external registration is cleaned up.

FAQ

Can SideEffect launch a coroutine?

No. Use LaunchedEffect for composition-bound suspend work or rememberCoroutineScope from an event handler.

Do I always need rememberUpdatedState with DisposableEffect?

No. Use it only for callbacks that should update without re-registering. Include dependencies as keys whenever their change must recreate the effect.