LaunchedEffect: When and How to Use It

Quick answer: Use LaunchedEffect to start a composition-bound coroutine for UI work such as collecting a transient stream, loading data for a changing ID, or an animation. Its keys define when that coroutine is cancelled and restarted. Include values that should restart the work; use rememberUpdatedState for changing values the work should read without restarting.

The Compose side-effects guide explains that LaunchedEffect launches a coroutine when it enters composition and cancels it when it leaves. It is not a general replacement for ViewModel work, callback handlers, or rendering state.

Treat keys as restart rules

@Composable
fun ArticleRoute(articleId: String, repository: ArticleRepository) {
    var article by remember { mutableStateOf<Article?>(null) }

    LaunchedEffect(articleId) {
        article = repository.loadArticle(articleId)
    }
}

When articleId changes, Compose cancels the old request coroutine and launches a new one. That is the desired rule: the visible article defines the work. A key that changes too often wastes work; a missing key leaves the old work attached to new UI.

RequirementKey choice
Reload when the account changesLaunchedEffect(accountId)
Collect a route-owned effect streamLaunchedEffect(effects)
Run once for the call site’s lifetimeLaunchedEffect(Unit)—only after deliberate review
Read the latest callback without restartingrememberUpdatedState(callback)

The official guidance is straightforward: variables used in an effect belong in its keys unless a change should not restart it; use rememberUpdatedState for that exception.

Keep a long-lived timer alive across callback changes

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import kotlinx.coroutines.delay

@Composable
fun TimedLanding(onTimeout: () -> Unit) {
    val currentOnTimeout by rememberUpdatedState(onTimeout)

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

The timeout belongs to this composable instance, so the constant key is intentional. The callback can change as the parent recomposes, but rememberUpdatedState lets the coroutine call the newest callback without restarting the delay. Android’s documentation warns that LaunchedEffect(true) or Unit deserves the same scrutiny as an infinite loop: it is valid only when lifetime-bound work is truly intended.

Use the right API for adjacent jobs

  • For a suspend action caused directly by a click, use rememberCoroutineScope() and launch from the event handler. A snackbar button is the usual example.
  • For a listener that must register and unregister, use DisposableEffect and clean up in onDispose.
  • For a Flow that represents durable Android UI state, use collectAsStateWithLifecycle; see collecting StateFlow with lifecycle.
  • For repository calls and business logic that should outlive a composable, use a ViewModel, then expose UI state.

Never launch work while rendering

// Wrong: composition can re-run or be discarded.
if (shouldRefresh) {
    repository.refresh()
}

Rendering should describe UI only. Move controlled UI work into LaunchedEffect, and send user intent to the state owner. UI state, events, and one-time effects shows that full boundary.

Common key mistakes

Keying with the whole mutable UI state

LaunchedEffect(uiState) restarts for every state transition, including unrelated ones. Key with the precise input that changes the job’s meaning, such as an ID or query.

Omitting a changed dependency

If a coroutine uses userId but keys only on Unit, it can continue running for the previous user. Add userId as a key unless it deliberately uses a latest-value reference.

Using LaunchedEffect for every click

Do not store a click in mutable state just to make an effect fire. Launch the suspend operation from the click handler’s composition scope when the action is local, or send an event to the ViewModel when business logic owns it.

Test the lifecycle contract

  • Change each restart key and verify the old job is cancelled and a new one starts.
  • Recompose with a changed callback and verify whether the job should restart or use the latest callback.
  • Remove the composable from the tree and confirm the work is cancelled.
  • Test empty, loading, error, and success rendering separately from effect delivery.

FAQ

Does LaunchedEffect run on every recomposition?

No. It runs when it first enters composition and restarts only when one of its keys changes. Ordinary recomposition with unchanged keys does not restart it.

Can I call navigation inside it?

Yes, when the navigation is a controlled one-time UI effect. Do not call navigation directly from the composable body, where recomposition can repeat the command.