Side Effects in Compose: LaunchedEffect, DisposableEffect, and Friends
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()
}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()
}