SideEffect — what it is, state synchronization in Jetpack Compose

Author: IT Sectr Published: 2026-06-30 Reading time: 9 min

SideEffect is a composable function in Jetpack Compose that executes the passed code block on every successful recomposition. Unlike LaunchedEffect and DisposableEffect, SideEffect is not tied to keys and has no cleanup block — it simply synchronizes Compose state with external systems after each rendering. This makes it ideal for updating callback functions, synchronizing with ViewPager, and passing data to Analytics SDK. According to Android Developers Documentation (2025), SideEffect executes strictly after Compose confirms successful recomposition, and does not execute if recomposition was skipped.

Key Takeaways

  • SideEffect — a side-effect API for code executed after each successful recomposition.
  • Synchronization — passes Compose state to external systems that do not support Compose.
  • No keys — unlike LaunchedEffect, SideEffect does not restart but executes on every recomposition.
  • No cleanup — SideEffect does not provide onDispose, it is designed only for one-way synchronization.
  • Synchronous — the block executes synchronously within the Compose composition phase, without coroutines.

What is SideEffect in Jetpack Compose

SideEffect is the simplest of the side-effect APIs in Jetpack Compose. It executes a code block on every successful recomposition of a composable component. The word “successful” is key here: if Compose decides that recomposition is not required (for example, all input parameters are unchanged and the result will be the same), SideEffect does not execute. This ensures the synchronization block is called only when the UI has actually changed.

The main use case for SideEffect is synchronizing Compose state with objects that are not part of the Compose tree. Typical examples include: updating a callback function in a Legacy View system, passing the current state to ViewPager, sending an event to an Analytics SDK when displayed data changes, and synchronizing with mapping SDKs that expect updates in an external format.

According to Android Developer Blog (2025), SideEffect is often used in conjunction with remember: remember preserves an object (e.g., a callback), and SideEffect updates it whenever a dependency changes. This pattern is especially important for libraries that accept listener objects and do not recreate them on update — without SideEffect, the listener would hold a stale reference to the current state.

kotlin
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
    val mapView = remember { MapView(LocalContext.current) }
    
    SideEffect {
        mapView.setZoom(zoomLevel)
        mapView.updateMarkers(markers)
    }
    
    AndroidView(factory = { mapView })
}

How SideEffect works and composition phases

To understand SideEffect, you need to understand the execution phases of Jetpack Compose. Compose goes through three phases for each frame: Composition (what to display), Layout (where to display), Drawing (how to display). SideEffect executes at the end of the Composition phase — after all composable functions have run, but before the Layout phase. This ensures that SideEffect sees the final state of all variables after recomposition.

This placement in the lifecycle gives an important advantage: SideEffect cannot cause infinite recomposition, even if state is changed inside it. Because it executes after composition, changes made inside SideEffect will only be applied in the next frame — this prevents the cycles typical of state changes inside the body of a composable function (when setState inside composition triggers a new composition before the current one finishes).

Another feature is that SideEffect is not optimized by keys. It executes on every recomposition regardless of which specific state changed. If you need more precise control (execute only when a specific parameter changes), use LaunchedEffect with keys or wrap SideEffect in a change check via remember.

kotlin
// SideEffect optimized with remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }

SideEffect {
    if (currentZoom != zoomLevel) {
        map.animateToZoom(zoomLevel)
        currentZoom = zoomLevel
    }
}

// Without this check SideEffect would call animateToZoom
// on every recomposition, even if zoomLevel didn't change

Updating callback functions with SideEffect

The most common practical scenario for SideEffect is updating callback functions that capture the current state. In Jetpack Compose this is called “callback lifecycle management”. The problem is that lambda expressions in Kotlin capture variables by reference, and if a callback was created with one value and later the variable changes — the callback continues to use the old value.

Consider an example: the Google Maps SDK for Android accepts an OnCameraMoveListener object via setOnCameraMoveListener(). If you pass a lambda that captures isTrackingEnabled, when isTrackingEnabled changes the lambda will not be updated — the Maps SDK will continue calling the old callback with stale data. SideEffect solves this problem: it re-sets the listener on every recomposition, ensuring the SDK always uses the current lambda with the latest state.

According to Maps SDK for Android Documentation (2025), Google recommends exactly this pattern when integrating Maps with Jetpack Compose. A similar approach is used for WebView, VideoView, TextureView, and any other View-based components that accept callbacks via set-methods. SideEffect ensures the callbacks are up to date with every state change.

kotlin
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
    val mapView = remember { MapView(LocalContext.current) }
    
    SideEffect {
        mapView.setOnMarkerClickListener { marker ->
            onMarkerClick(marker)
            true
        }
        mapView.isTrafficEnabled = isTrackingEnabled
    }
    
    AndroidView(factory = { mapView })
}

Synchronization with Analytics SDK

Another important scenario for SideEffect is sending events to analytics systems when the UI state changes. For example, when a user switches tabs in a TabLayout inside a Compose screen, SideEffect can pass the current selected tab state to Firebase Analytics or AppsFlyer. Every time the selected tab changes (and recomposition occurs), SideEffect sends the corresponding event.

The difference from sending events directly in onClick or onTabSelected is that SideEffect triggers on state changes from any source — not only user actions but also programmatic changes, state restoration after screen rotation, or Deep Links. This makes SideEffect a universal synchronization mechanism independent of the change source.

According to Firebase Best Practices (Google, 2025), sending analytics events via SideEffect provides a more complete picture of the user journey, since it captures all state changes, including those that occur without direct user action. However, it is important not to overdo it: each analytics event is a network request, so for frequently changing states (scroll position, finger coordinates) SideEffect is not suitable — use debounce or send events only on significant changes.

kotlin
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
    val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
    
    SideEffect {
        val params = Bundle().apply {
            putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
            putString(FirebaseAnalytics.Param.ITEM_ID, productId)
        }
        firebaseAnalytics.logEvent(        FirebaseAnalytics.Event.VIEW_ITEM, params)
    }
    
    // UI with TabRow and selected tab
}

SideEffect vs LaunchedEffect: when to use what

The choice between SideEffect and LaunchedEffect depends on two factors: whether asynchronous execution is needed and whether key-based control is needed. SideEffect is synchronous and executes on every recomposition. LaunchedEffect is asynchronous (coroutine) and executes only when a key changes, not on every recomposition.

If you need to perform an action on every UI change — use SideEffect. If you need to perform an action once when the screen appears or when a specific parameter changes — use LaunchedEffect with keys. If an asynchronous operation is required (data loading, delay, working with Flow) — only LaunchedEffect, as SideEffect does not support suspend functions.

CharacteristicSideEffectLaunchedEffect
ExecutionOn every recompositionOn key change
AsynchronySynchronousCoroutine
KeysNoYes (vararg)
CleanupNoAutomatic coroutine cancellation
Typical useCallbacks, Analytics, View synchronizationLoading, Flow subscription, timers

In practice, 70% of side-effect use cases are covered by LaunchedEffect (asynchronous operations, data loading), 20% by DisposableEffect (resources with cleanup), and only 10% by SideEffect (callback synchronization). SideEffect is a specialized tool for a narrow range of tasks, but in those tasks it is indispensable.

Common mistakes with SideEffect

The main mistake is changing Compose state inside SideEffect. Although SideEffect does not cause an infinite loop directly (since it executes after the composition phase), it can trigger excessive recompositions. If state is changed inside SideEffect (mutableStateOf), it triggers a new recomposition in the next frame, which again executes SideEffect — and so on until stabilization. This is not an infinite loop, but unnecessary work for the framework.

The second mistake is performing heavy computations inside SideEffect. Since SideEffect is called on every recomposition, and recompositions can happen dozens of times per second (during animations, scrolling), any heavy code inside SideEffect will lead to frame drops. Move heavy operations outside composition — into a coroutine (LaunchedEffect) or compute via derivedStateOf / remember.

The third mistake is trying to use SideEffect for asynchronous code. SideEffect is not a suspend function, so delay(), await(), collect(), and other coroutine operations inside it will not compile. If you need to perform an asynchronous action after recomposition, use snapshotFlow { ... } in combination with LaunchedEffect, or launch a coroutine via rememberCoroutineScope.

Frequently Asked Questions

Does SideEffect execute on first composition?

Yes, SideEffect executes on every successful composition, including the very first one — when the component first appears on the screen. This differs from LaunchedEffect(Unit), which also executes once on first composition but does not execute on subsequent recompositions (if the key has not changed).

Can SideEffect cause an infinite loop?

No, SideEffect executes after the composition phase — changes made inside it will only be applied in the next frame, which prevents loops. However, frequently changing state inside SideEffect can cause a cascade of recompositions, reducing performance. Only change state inside SideEffect when absolutely necessary.

What is the difference between SideEffect and snapshotFlow?

SideEffect executes synchronously on every recomposition. snapshotFlow creates a Flow from Compose state and can be used with collectLatest in LaunchedEffect for reactive change handling. snapshotFlow is suitable for cases where you need to react to changes with debounce, filter, or distinctUntilChanged — which is impossible in synchronous SideEffect.

How to debug SideEffect if it executes too often?

Use Android Studio Compose Modifier Debugger or add logging with the component name and call frequency. If SideEffect executes more often than expected, check whether the parent component’s state is changing unnecessarily. Optimization: extract stable parts of the UI into separate composable functions with unstable annotations to reduce the number of recompositions.

Can SideEffect be combined with DisposableEffect?

Yes, they can be used in the same component for different purposes. DisposableEffect is responsible for setting up and cleaning up a resource (once), while SideEffect handles synchronizing the current state with that resource on every recomposition. A typical example: DisposableEffect registers a callback via API, and SideEffect updates the captured data in that callback on every change.

Summary

  • SideEffect — a side-effect API for synchronous code executed after each successful recomposition in Jetpack Compose.
  • Synchronization — the main use case: passing Compose state to external systems (Google Maps, WebView, ViewPager, Analytics SDK).
  • Callbacks — SideEffect ensures callback functions that capture current state remain up to date with every UI update.
  • No loops — executes after the composition phase, so changing state inside SideEffect does not cause infinite recomposition.
  • Limitations — does not support keys, asynchrony, or a cleanup block; for these tasks use LaunchedEffect or DisposableEffect.
  • Performance — avoid heavy computations inside SideEffect, as it executes on every recomposition (up to 60 times per second).
  • Debugging — control call frequency via Compose Debugger and optimize with remember to filter unnecessary recompositions.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also