Side Effects in Compose: LaunchedEffect, DisposableEffect, and Friends

By Engineering Team • Published: 2025-12-14 • Updated: 2025-12-1412 min read

Side EffectsLifecycleCoroutinesBest Practices

What Are Side Effects in Compose?

In Compose, a side effect is any operation that escapes the scope of a composable function — network calls, database writes, analytics events, or anything that should persist beyond recomposition. Since composables can recompose frequently and unpredictably, uncontrolled side effects lead to bugs.

Compose provides dedicated APIs to manage side effects safely. Each has a specific use case, and choosing the wrong one is a common source of subtle bugs.

LaunchedEffect: One-Shot Coroutines

LaunchedEffect launches a coroutine when it enters composition and cancels it when leaving. Use it for one-shot operations triggered by key changes:

@Composable
fun UserProfile(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }
    
    // Launches when userId changes, cancels previous if still running
    LaunchedEffect(userId) {
        user = repository.fetchUser(userId)
    }
    
    user?.let { ProfileContent(it) } ?: LoadingSpinner()
}
Note: Always provide meaningful keys to LaunchedEffect. Using Unit as key means it runs once on first composition only.

DisposableEffect: Setup and Cleanup

DisposableEffect is for operations that require cleanup — listeners, callbacks, or resources that must be released:

@Composable
fun LocationTracker(onLocationChanged: (Location) -> Unit) {
    val context = LocalContext.current
    
    DisposableEffect(Unit) {
        val locationManager = context.getSystemService<LocationManager>()
        val listener = LocationListener { location ->
            onLocationChanged(location)
        }
        
        locationManager?.requestLocationUpdates(
            LocationManager.GPS_PROVIDER,
            1000L,
            10f,
            listener
        )
        
        onDispose {
            locationManager?.removeUpdates(listener)
        }
    }
}

rememberCoroutineScope: User-Triggered Actions

For coroutines triggered by user actions (not composition), use rememberCoroutineScope. Unlike LaunchedEffect, it survives recomposition:

@Composable
fun SubmitButton(onSubmit: suspend () -> Result<Unit>) {
    val scope = rememberCoroutineScope()
    var isLoading by remember { mutableStateOf(false) }
    
    Button(
        onClick = {
            scope.launch {
                isLoading = true
                onSubmit()
                isLoading = false
            }
        },
        enabled = !isLoading
    ) {
        if (isLoading) CircularProgressIndicator() else Text("Submit")
    }
}

SideEffect: Sync with Non-Compose Code

SideEffect runs on every successful recomposition. Use it to sync Compose state with non-Compose systems like analytics or logging:

@Composable
fun AnalyticsScreen(screenName: String, content: @Composable () -> Unit) {
    SideEffect {
        // Runs on every successful recomposition
        analytics.trackScreenView(screenName)
    }
    content()
}
Note: SideEffect should never trigger recomposition or modify state. It is purely for syncing with external systems.
← Back to Blog