Side Effects in Jetpack Compose

In Jetpack Compose, side effects refer to operations that cause a change in the outside world or system state.

In the context of Jetpack Compose, side effects are necessary to interact with the environment, like triggering a network request, modifying a UI element, or interacting with a system service.

Jetpack Compose provides a set of APIs for handling side effects, and knowing when and how to use them is crucial for writing maintainable and efficient applications.

1. LaunchedEffect

What It Is:

LaunchedEffect is a side effect API used in Compose for launching coroutines that react to changes in keys or trigger side effects within the composable scope.

When to Use:

  • When you need to launch a coroutine in response to changes in a specific key.

  • For performing one-time effects (like fetching data, starting animations, etc.) when a composable is first composed or when a certain state changes.

How to Use:

LaunchedEffect takes a key as its parameter. It runs the provided block of code only when the key changes. It is the recommended API for performing side effects within a composable.

@Composable
fun DataFetcher(key: String) {
    LaunchedEffect(key) {
        // Simulating network call or data fetch
        val data = fetchDataFromNetwork(key)
        // Do something with the data (e.g., update state)
    }
}

What Not To Do:

  • Avoid launching long-running operations in LaunchedEffect that might block the main thread. Always ensure that the work is offloaded to a background thread.

  • Don't use LaunchedEffect for actions that you expect to happen repeatedly. Consider using rememberUpdatedState for state changes that don’t require side effects.

2. SideEffect

What It Is:

SideEffect allows you to run a block of code every time the composition is recomposed. It is a more lightweight side effect compared to LaunchedEffect and doesn’t use coroutines.

When to Use:

  • When you need to trigger side effects that are not coroutines-based.

  • For actions like logging, updating analytics, or performing UI updates that are dependent on the composition state.

How to Use:

SideEffect runs every time the composable recomposes, making it suitable for quick side effects like logging or updating UI-related states.

@Composable
fun Counter(counter: Int) {
    // Runs every time the composable recomposes
    SideEffect {
        println("Counter value has changed to: $counter")
    }
    Text("Counter: $counter")
}

What Not To Do:

  • Don’t perform expensive operations inside SideEffect because it runs on each recomposition, which could lead to unnecessary overhead.

  • Avoid putting side effects that need to run once or after certain actions. Use LaunchedEffect for those cases.

3. rememberUpdatedState

What It Is:

rememberUpdatedState is used to remember the current state of a variable across recompositions. It ensures that the latest value of a variable is always used without causing unnecessary recompositions.

When to Use:

  • When you need to keep a reference to a value that changes but should not trigger recompositions when that value changes.

  • For side effects that depend on changing state or variables but don’t want to re-trigger the effect unnecessarily.

How to Use:

rememberUpdatedState keeps track of the latest value, and the composable only recomposes when the composable itself is recomposed.

@Composable
fun ExampleWithUpdatedState() {
    var counter by remember { mutableStateOf(0) }
    val updatedCounter = rememberUpdatedState(counter)

    // Perform some action on the updated value without triggering recompositions
    Button(onClick = {
        // Using updated counter value
        someAction(updatedCounter.value)
    }) {
        Text("Increase Counter")
    }
}

fun someAction(counterValue: Int) {
    // Use the updated value for non-recomposable operations
}

What Not To Do:

  • Don’t use rememberUpdatedState as a replacement for triggering state changes in the UI. It's meant for holding and using values across recompositions without forcing recomposition itself.

4. DisposableEffect

What It Is:

DisposableEffect is used when you need to set up resources that should be disposed of when the composable leaves the composition, such as registering listeners or opening a file. It provides a cleanup block that runs when the composable is removed from the composition.

When to Use:

  • When you need to set up resources that require cleanup.

  • For managing lifecycle-sensitive resources such as event listeners, sensors, or observers.

How to Use:

DisposableEffect takes a key and ensures that the setup and cleanup are tied to the lifecycle of the composable.

@Composable
fun EventListener() {
    // Set up event listener that needs cleanup on disposal
    DisposableEffect(Unit) {
        val listener = MyEventListener()
        registerListener(listener)

        // Clean up the listener when the composable is removed
        onDispose {
            unregisterListener(listener)
        }
    }
}

What Not To Do:

  • Don’t use DisposableEffect for tasks that don’t need explicit cleanup, like simple UI updates or non-lifecycle-dependent tasks.

  • Avoid registering or disposing of resources on every recomposition if the effect doesn’t rely on the composition lifecycle.

5. rememberCoroutineScope

What It Is:

rememberCoroutineScope is used to obtain a coroutine scope that is tied to the composable’s lifecycle. It allows launching coroutines within a composable and is useful for handling side effects like network requests or long-running operations.

When to Use:

  • When you need to launch coroutines directly within a composable, ensuring they are canceled when the composable is removed from the composition.

How to Use:

You can use the coroutine scope to launch and manage long-running tasks like network requests or background operations.

@Composable
fun CoroutineScopeExample() {
    val coroutineScope = rememberCoroutineScope()

    Button(onClick = {
        coroutineScope.launch {
            // Long-running task (e.g., fetching data)
            val data = fetchDataFromNetwork()
            // Handle the data (e.g., update UI state)
        }
    }) {
        Text("Fetch Data")
    }
}

What Not To Do:

  • Don’t use rememberCoroutineScope for tasks that should not be tied to the composable lifecycle. For tasks like background work that should continue independently, use other coroutine scopes (like GlobalScope or custom scopes).

  • Avoid launching coroutines inside @Composable functions that don’t need to be aware of the lifecycle of the composable.

Visualization of Execution Order

@Composable
fun MyComposable(counter: Int) {
    // 1st: remember state
    val updatedCounter = rememberUpdatedState(counter)

    // 2nd: SideEffect (runs after recomposition)
    SideEffect {
        println("SideEffect: Counter updated to $counter")
    }

    // 3rd: LaunchedEffect (coroutine runs after composition)
    LaunchedEffect(counter) {
        delay(1000)  // Simulating background work
        println("LaunchedEffect: Counter is $counter after delay")
    }

    // UI updates (Composables that react to state)
    Text("Counter: $counter")

    // 4th: DisposableEffect (runs during setup and cleanup)
    DisposableEffect(counter) {
        val listener = MyEventListener()
        registerListener(listener)

        // Cleanup when removed from composition
        onDispose {
            unregisterListener(listener)
        }
    }

    // 5th: Coroutine launched using rememberCoroutineScope (user action triggers coroutine)
    val coroutineScope = rememberCoroutineScope()
    Button(onClick = {
        coroutineScope.launch {
            println("Coroutine launched from rememberCoroutineScope: Counter is $counter")
        }
    }) {
        Text("Launch Coroutine")
    }
}

Execution Flow:

  1. rememberUpdatedState is called first to remember the latest value of the state.

  2. SideEffect runs to log any immediate UI changes that occur due to recomposition.

  3. LaunchedEffect starts a coroutine asynchronously after the UI updates (e.g., fetching data or triggering animations).

  4. DisposableEffect handles resource cleanup if the composable is disposed, such as unregistering event listeners or cleaning up resources.

  5. rememberCoroutineScope is used to launch coroutines in response to user actions, keeping them independent of the composable's lifecycle.

Key Points to Remember:

  • SideEffect runs every time the composable recomposes and does not involve coroutines.

  • LaunchedEffect runs asynchronously and responds to key changes. It’s ideal for coroutines.

  • DisposableEffect is used for setting up and cleaning up resources tied to the composable’s lifecycle.

  • rememberCoroutineScope provides a scope for launching coroutines that can be triggered by user interactions.

Conclusion

Understanding the appropriate time to use each effect ensures that you maintain clean and efficient code, avoiding unnecessary recompositions and keeping your app’s performance in check.

In summary:

  • Use LaunchedEffect for launching coroutines that react to key changes.

  • Use SideEffect for non-coroutine side effects triggered on recomposition.

  • Use rememberUpdatedState for tracking updated values without forcing recompositions.

  • Use DisposableEffect for managing lifecycle-related resources that need cleanup.

  • Use rememberCoroutineScope for launching coroutines tied to the composable’s lifecycle.

Akshay Nandwana
Founder AndroidEngineers

You can connect with me on:


Book 1:1 Session here
Click Here

Join our upcoming classes
https://www.androidengineers.in/courses

2025© Made with   by Android Engineers.