“derivedStateOf” in Compose
-
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
variableval 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:
-
In the case of
remember(stateA, stateB)
, thecombineStates
function will unconditionally be called the first time and the value remembered. If either ofstateA
orstateB
changes, the function will be called additionally every time. This happens regardless of the value ofsomeCondition
(i.e., regardless if we will read frommyState
). Additionally, when the value ofmyState
does change (i.e., when A or B have changed), it will necessitate a recomposition ofItemsFromMyState
and its child composable since the local state has changed and the child (ultimately theLazyColumn
) 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 theLazyColumn
's lambda where it will be used. -
In the case of
derivedStateOf
, nothing happens initially during the composition ofItemsFromMyState
until we reach the point where we would read frommyState
(i.e., inside theLazyColumn
, ifsomeCondition
was true). At that point, we would callcombineStates
and store the returned value. Future reads ofmyState
would not call thecombineStates
function unless A or B have changed. If A or B changes, then any reads ofmyState
in the same (or future) snapshots will include these changes (i.e., will have calledcombineStates
with the updated values). Note that ifsomeCondition
was false, none of this would happen. Additionally, ifcombineStates
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 onmyState
are recomposed, i.e. in this case theLazyColumn
.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 State
s, 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 State
s. 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 remember
ed 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
State
s. 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 usemyState
). Even worse, since we’re remembering the derived state, it will remain unchanged asa
andb
change! The correct way to handle this here would be to either pass or refer to theState
s themselves inInner
, or to forego usingderivedStateOf
in favor ofremember(a, b)
. -
If you are reading your derived state in the same scope, there is no value to using
derivedStateOf
over a keyedremember
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
andmyState
in the same scope. This means if either of A or B change, theFoo
composable will be recomposed anyway, and we would have been able to use a keyedremember
. However, ifmyState
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.