DisposableEffect, SideEffect, and rememberUpdatedState Compared

Quick answer: Use
DisposableEffectto register something that must be unregistered,SideEffectto publish successfully composed state to non-Compose code, andrememberUpdatedStateto 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
| API | Use it when | Key behavior |
|---|---|---|
DisposableEffect | An observer, listener, receiver, or resource requires cleanup | Dispose on key change or leaving composition |
SideEffect | A non-Compose object needs the latest successfully rendered value | Runs after every successful recomposition |
rememberUpdatedState | A running effect needs the latest callback/value but must not restart | Updates 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
DisposableEffectkeys when a new dependency should replace the old registration. - Do not use
rememberUpdatedStateto 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.