rememberCoroutineScope

Every side effect we've covered so far — LaunchedEffect, DisposableEffect, produceState — is something you write directly in the body of a composable function. But a lot of coroutine work is triggered by a one-time user action, like tapping a button, and an onClick lambda is not composable context — you cannot call LaunchedEffect from inside it, because LaunchedEffect only makes sense as part of composition itself, not as a response to an event. rememberCoroutineScope() is the bridge for exactly this situation: it hands you a CoroutineScope you can freely launch coroutines from, anywhere, including from inside a click handler.

Why LaunchedEffect Doesn't Work Here

LaunchedEffect starts its coroutine as part of composition and re-evaluates its key on every recomposition — it's designed to answer "what coroutine should be running given the current state," not "run this once, right now, because the user just clicked something." Trying to call it from inside Button(onClick = { LaunchedEffect(...) }) won't even compile: onClick is a plain lambda, not a @Composable function.

rememberCoroutineScope(): A Scope Tied to the Composition

Calling rememberCoroutineScope() in a composable's body gives you back a CoroutineScope that survives recomposition (thanks to remember) and is automatically cancelled when the composable leaves composition — the same lifecycle guarantee LaunchedEffect gives you, just packaged as a scope you keep around and call .launch { } on whenever you need to, rather than something Compose runs for you automatically.

@Composable
fun SomeScreen() {
    val scope = rememberCoroutineScope()

    Button(onClick = {
        // Launch a coroutine from a plain click handler —
        // not possible with LaunchedEffect, which isn't callable here
        scope.launch {
            doSomeSuspendWork()
        }
    }) {
        Text("Do it")
    }
}

Worked Example: Showing a Snackbar From a Button Click

Showing a Material Snackbar is itself a suspend call — SnackbarHostState.showSnackbar() suspends until the snackbar is dismissed or times out. That makes it a perfect fit for rememberCoroutineScope: RecipeBox's Add/Edit Recipe screen wants to confirm a save with a Snackbar the instant the user taps "Save," directly from that button's onClick.

@Composable
fun AddEditRecipeScreen(viewModel: AddEditRecipeViewModel, recipe: Recipe) {
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) }
    ) { padding ->
        Column(modifier = Modifier.padding(padding)) {
            // ...title, ingredients, steps, category, prepMinutes fields

            Button(onClick = {
                viewModel.saveRecipe(recipe)

                // showSnackbar() is a suspend function — we need a scope to call it
                // from this plain onClick lambda, so we launch it on our remembered scope
                scope.launch {
                    snackbarHostState.showSnackbar("Recipe saved")
                }
            }) {
                Text("Save Recipe")
            }
        }
    }
}

If the user navigates away from this screen right after tapping Save, the scope returned by rememberCoroutineScope() is cancelled along with the rest of the composition — so the in-flight Snackbar coroutine is cleaned up automatically, exactly the same safety net LaunchedEffect and DisposableEffect give you, just triggered manually instead of running as part of composition itself.

Next up: We've now covered every major side-effect API in this track. Choosing the Right Side Effect pulls them all into one decision table with realistic RecipeBox scenarios to test yourself against.