Compose Side Effects Explained: LaunchedEffect, DisposableEffect and SideEffect

A composable function is supposed to be free of side effects — it just describes UI based on its inputs, and Compose may call it more often than you’d expect, or skip calling it, as part of recomposition. But real apps need to do things like start a coroutine, register a listener, or show a Snackbar in response to an event. Compose’s effect APIs are the sanctioned way to do that safely.

LaunchedEffect: Running Suspend Code Tied to the Composition

LaunchedEffect launches a coroutine when it first enters the Composition, and cancels it automatically when it leaves. Give it a key: if the key changes between recompositions, the running coroutine is cancelled and a new one is launched with the new key.

@Composable
fun UserProfile(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }

    LaunchedEffect(userId) {
        user = repository.fetchUser(userId)
    }

    if (user != null) {
        Text(user!!.name)
    }
}

Here, passing userId as the key means that if the composable is re-shown with a different userId, the old fetch is cancelled and a fresh one starts — instead of racing an old request against a new one.

DisposableEffect: Cleaning Up Non-Compose Resources

DisposableEffect is for effects that need explicit cleanup when they leave the Composition — registering and unregistering a broadcast receiver, a lifecycle observer, or any callback-based API that isn’t coroutine-based. It must end with an onDispose block.

@Composable
fun rememberNetworkStatus(): State<Boolean> {
    val isConnected = remember { mutableStateOf(true) }

    DisposableEffect(Unit) {
        val listener = NetworkListener { connected -> isConnected.value = connected }
        listener.register()

        onDispose {
            listener.unregister()
        }
    }

    return isConnected
}

Forgetting the onDispose block — or forgetting to unregister inside it — is a common source of leaked listeners in Compose code.

SideEffect: Publishing State to Non-Compose Code

SideEffect runs on every successful recomposition, with no key and no cancellation. It’s the right tool when you need to push a Compose value out to something Compose doesn’t manage, such as an analytics library that expects to be updated on every value change:

@Composable
fun ScreenView(screenName: String) {
    SideEffect {
        analytics.setCurrentScreen(screenName)
    }
}

Which One Should You Use?

  • Need to call suspend functions (network requests, delays, Flow collection)? Use LaunchedEffect.
  • Need to register/unregister a callback-based listener? Use DisposableEffect, and don’t skip onDispose.
  • Need to sync a value to non-Compose code on every recomposition, with no cleanup needed? Use SideEffect.

If none of these fit and you just need to compute a value from other state, you likely don’t need an effect at all — see our guide on derivedStateOf instead.