“derivedStateOf” in Compose

“derivedStateOf” in Compose

19.11.2024 8min read
  • Anders Järleberg

Note: This article was originally published in 2022 and has been updated as of November 2024. 

Understanding how to manage State effectively is crucial for building performant Compose apps. In this blog post, I explore the nuances of derivedStateOf, its role in reducing recompositions, and when to use it over alternatives like keyed remember. If you want to optimize your Compose app's state handling, this post offers practical insights and examples to guide you.

State and derived state in Compose

If you saw the video on performance best practices in Compose from Google I/O 2022, you saw the recommendation to use derivedStateOf when appropriate to reduce the number of recompositions that may be needed. Even while reading the documentation, it was not clear to me when and how it should be used over alternatives in general. After spending some time researching it, I want to share my understanding of derivedStateOf in Compose and hope to provide a useful mental model for its function and use.

One of the fundamental parts of Compose is how it handles state (and updates to it) to produce our UI structure and contents, and it’s important to have the right understanding to produce correct and performant composables. Let’s start by looking at the basics of state in Compose, the use of remember, and finally, how and when to use derivedStateOf.

State in Compose

The typical ways that Compose can know that a composable is dependent on a certain state are:

  • Passing a value as a parameter

    @Composable
    fun Name(name: String) {
    Text(name) // Our UI depends on the state of the passed parameter
    }
  • Using a State variable

    val name: State<String>

    @Composable
    fun Name() {
    Text(name.value) // Our UI depends on the current value in the `name` state
    }

In both of these examples, whenever the state would change, Name would be called again and reflect the new state, thus emitting an updated UI. It’s important to remember that this is actually code that executes, i.e., the Name function is executed again from the top. Unlike an XML UI, we’re not declaring objects once and then using them; we’re creating functions that depend on their input.

In the rest of this article, when we read the value of a State, the.value will be omitted for brevity (as if we were using a delegated property).

Note that I used a read-only state as input in the second example. Another common pattern you’ll see for (local) mutable state is:

@Composable
fun EditableName() {
var name by remember { mutableStateOf("") }
// Our UI depends on the current value in the `name` state,
// which may be edited by the user
TextField(value = name, onValueChange = { name = it })
}

This is essentially the same case, Compose can see that the TextField is dependent on the value in the state of Name (through Kotlin property delegation to the State with the by keyword). Let’s take a closer look at why the remember function is important.

The role of remember

Let’s say we’re showing a list of names, and we want each name to have its own (random) color. Naively we might write the following:

@Composable
fun Name(name: String) {
val color = randomColor()
Text(name, color = color)
}

This would appear to work, but one problem shows itself when we think about what happens if the name (or any other dependent state) changes, which would cause this function to recompose: we would generate a new random color. In this case, that might be fine if the name has changed, but in reality, there could be many reasons a composable is recomposed. To prevent the color from changing, we might use the remember function, as such:

@Composable
fun Name(name: String) {
val color = remember { randomColor() }
Text(name, color = color)
}

The remember function provides us with what’s called positional memoization – we remember this value at this specific position in the tree. When this function is recomposed (i.e., executed again), we have access to the value we defined previously at this position, so even when the name changes, the color does not.

However, this has another possibly unintended side effect. Let’s say we’re showing a row of three names: Alice (red), Bob (blue), and Carol (green). If we were to remove the first name in this list, we would end up with Bob (red) and Carol (blue), that is, the color is tied to the position and not the name itself.

The solution here is to generate the random color from the name and key the memoization to this as an input:

@Composable
fun Name(name: String) {
val color = remember(name) { randomColorFor(name) }
Text(name, color = color)
}

Note the name parameter passed to remember. The way to read this is that we want to remember the generated value at this position in the tree as long as the name is unchanged. The reason we have to generate the color from the name is that the memoization is still positional, so if the position changed as in the above example with Bob and Carol, we would have new colors unless the color is determined by the name. This also guarantees that if we use the Name composable in other parts of the tree, we would have identical colors there for the same name.

You may be asking “If the color is determined by the name, why do we need to remember it?” In this case, it’s an optimization to skip evaluating the randomColorFor function every time we recompose (memoizing the value).

In the previous section, I showed an example of a mutable state. Hopefully, it’s clear to you why we needed to remember the MutableState (we shouldn’t create a new state “holder” every time), and why it wasn’t keyed to the value (we should keep the state holder around regardless of its current value).

Memoizing a calculation based on the correct set of input keys is important to the correctness and performance of a composable. This is essentially axiomatic in how Compose works, in the sense that if the input that a function takes is unchanged since last time we called it, we don’t need to execute it again (assuming no side effects).

Keyed remember vs derivedStateOf

This brings us to one of my initial questions: what is derivedStateOf and when should we use it over a keyed remember?

First, let’s look at what derivedStateOf does: it takes a lambda and returns a new state whose values will be the return value from your lambda. Reading from this state will not re-calculate the value unless one of the states that your lambda depends upon has changed. What this means in practice might not be totally clear, so let’s look at some examples.

To begin with, let’s say we want a state that is based on two other states. What’s the difference between these two examples?

val stateA: State<A>
val stateB: State<B>

// 1.
val myState = remember(stateA, stateB) { combineStates(stateA, stateB) }
// 2.
val myState by remember { derivedStateOf { combineStates(stateA, stateB) } }

In both these cases, reading from myState will give us the combined state of A and B. Whenever the states of A or B change, myState will receive a new value. In this case, the two alternatives seem identical. The potential difference between these two actually lies in how myState will be used later on, not seen in this example. If we just read from myState directly in the same composable, there is no difference. However, let’s consider this case:

@Composable
fun ItemsFromMyState() {
val myState // myState defined in either way as above

Surface {
if (someCondition) {
LazyColumn {
items(myState.items) { /* ... */ }
}
} else { /* ... */ }
}
}

Here, we are not directly reading from myState in the same scope. Instead, we refer to it later on, and only conditionally so. Here is where one of the major differences becomes visible:

  1. In the case of remember(stateA, stateB), the combineStates function will unconditionally be called the first time and the value remembered. If either of stateA or stateB changes, the function will be called additionally every time. This happens regardless of the value of someCondition (i.e., regardless if we will read from myState). Additionally, when the value of myState does change (i.e., when A or B have changed), it will necessitate a recomposition of ItemsFromMyState and its child composable since the local state has changed and the child (ultimately the LazyColumn) is dependent on it.

    You can compare this with the previous example of passing a state as a parameter to a composable. We essentially have an invisible myState parameter passed all the way down to the LazyColumn's lambda where it will be used.

  2. In the case of derivedStateOf, nothing happens initially during the composition of ItemsFromMyState until we reach the point where we would read from myState (i.e., inside the LazyColumn, if someCondition was true). At that point, we would call combineStates and store the returned value. Future reads of myState would not call the combineStates function unless A or B have changed. If A or B changes, then any reads of myState in the same (or future) snapshots will include these changes (i.e., will have called combineStates with the updated values). Note that if someCondition was false, none of this would happen. Additionally, if combineStates does not produce a value different from the previous one, nothing else would happen. There is no recomposition of the composable or its children. If it does produce a different value, only the children that depend on myState are recomposed, i.e. in this case the LazyColumn.

    This is comparable to the other example of passing state mentioned earlier: reading from a State variable inside the composable. We’re not passing values along in the chain, we’re creating a state which we can then read from if needed.

Combining keyed remember and derived state

Of course, the two are not mutually exclusive. Derived state should be used when reading from States, but if the derived state is also dependent on non-State values, for correctness, you must key the remember appropriately:

val state: State<String>

@Composable
fun Foo(input: String) {
val derived by remember(input) { derivedStateOf { input + state } }
// ...
}

In the above example, we must key the remember to the value of input, as we will read it inside the derived state lambda. If we didn’t do this, we would keep remembering the old derived state if input changed, and it would not be recomputed since derivedStateOf is only notified of updates to States. This example also assumes that input would change less often than state. If the two would change equally often or if input changes more often than state, you could just use remember(input, state) instead.

Mental model for derived state

Derived state is its own State and should be treated as such, e.g. it needs to be remembered or stored in a ViewModel or similar.

derivedStateOf is like a lazy

The lambda you pass to derivedStateOf is only evaluated if it’s needed, and the value is memoized. However, unlike, it can (and should) change in response to other states. When dependent states change and the value is read, the lambda will be called again with the updated values. The returned value may still be the same, though, in which case nothing else happens.

derivedStateOf is like its own composable

You can think of the lambda you pass as its own composable scope, with dependent states and a resulting return value. When dependent states change, the lambda is called again, but it doesn’t recompose the surrounding composable(s) (unless they’re directly reading the same state). If your lambda returns the same value as last time, nothing else will be recomposed. If the value does change, only the composables that read the derived state need to recompose.

Derived state can be an optimization

As you may have seen in the IO session, the two guidelines above explain how we can optimize a state that depends on other states. One example is if you want a state that tells you if a list is scrolled to the top – instead of depending on the current scroll position directly (which will change very often and cause recompositions), you can depend on a derived state that tells you if the scroll position is at the top (whose resulting value only changes if the scroll reaches or leaves the top, thus causing fewer recompositions during a scroll):

@Composable
fun ScrollingList() {
val scrollState = rememberScrollState()
 val isScrolledToTop by remember { derivedStateOf { scrollState.value == 0 } }
if (isScrolledToTop) { /* ... */ }
}

That being said, it’s not a catch-all optimization you can always apply to reduce recompositions.

Derived state is not always an optimization

There are a few important things to keep in mind for derived state to actually improve performance, and to ensure correctness of your composable.

  • Your lambda generating the derived state must read from other States. If you are reading directly from a parameter, as far as Compose is concerned, your composable is dependent on these values regardless of what you do in your lambda, e.g.

    val stateA: State<A>
    val stateB: State<B>

    @Composable
    fun Outer() {
    Inner(stateA, stateB)
    }

    @Composable
    fun Inner(a: A, b: B) {
     val myState by remember { derivedStateOf { combine(a, b) } }
    // ...
    }

    In the above example, if stateA or stateB changes, Inner is unconditionally recomposed (regardless of how we use myState). Even worse, since we’re remembering the derived state, it will remain unchanged as a and b change! The correct way to handle this here would be to either pass or refer to the States themselves in Inner, or to forego using derivedStateOf in favor of remember(a, b).

  • If you are reading your derived state in the same scope, there is no value to using derivedStateOf over a keyed remember if you’re already (unconditionally) reading all of its dependent states, e.g.

    @Composable
    fun Foo() {
    val myState by remember { derivedStateOf { combine(stateA, stateB) } }
    Text("A: $stateA, B: $stateB, myState: $myState")
    }

    In the above example, we’re reading from stateA, stateB and myState in the same scope. This means if either of A or B change, the Foo composable will be recomposed anyway, and we would have been able to use a keyed remember. However, if myState was only conditionally read (or used further down the tree), that could still provide some benefit as described above (”lazy” evaluation).

As always, when discussing optimization, measure first to determine if there is a problem. If you identify a problem, make sure you understand what’s happening and why. Hopefully, these points have given you the mental model to understand what situations derived state may be useful.

Conclusion

Generally speaking, it’s probably quite rare that you will need to reach for derivedStateOf it in your Compose app, but in some specific scenarios, it’s exactly what you want. Knowing how state works in Compose and thus knowing when and how to use the tools available to you for the optimal (and bug-free) behavior is always going to be of help in your daily work. Hopefully, this post has given you some pointers along the way to deeper understanding of state in Compose.