useCallback
useCallback
is a hook that allows a parent to pass a child component a callback which:
- maintains referential equality across multiple layout passes
- is updated to have the latest parent props and state, even if the child doesn't re-render.
Exampleβ
In the following code, we have a parent component which renders a list of children via a Collection. Let's say we also want this parent to implement multi-select behavior, meaning on click, a row can be marked as selected/de-selected. We will store the state of which components are currently selected in the parent, and the parent will pass a lambda to the child to allow that state to be updated when a row is clicked.
When the first row is clicked, it will update the state in the parent via an onClick lambda. At this point, we ideally want to re-render only that row since that's the only row whose UI will be updated, i.e. to a selected state. However, that means the rest of the children will still have that original onClick lambda set on them: if that lambda captured props/state, then invoking it will operate on stale props/state! In this case, that means selecting a second row will de-select the first one which is incorrect.
An alternative is to always re-render all children whenever the list of selected items changes. This is effective in that it will make sure all children have a lambda capturing the latest props/state, however it's inefficient since it re-renders all children even though their UI doesn't appear different.
useCallback
tries to give the best of both worlds: it doesn't cause children to re-render, and
also gives a mechanism for them to invoke a lambda that has captured the latest props and state
class SelectionCollectionKComponent : KComponent() {
private val items = listOf("O-Ren Ishii", "Vernita Green", "Budd", "Elle Driver", "Bill")
override fun ComponentScope.render(): Component {
val selected = useState { setOf<String>() }
fun isSelected(itemIndex: String): Boolean = selected.value.contains(itemIndex)
fun isAllSelected(): Boolean = selected.value.size == items.size
val selectItemClickCallback = useCallback { item: String ->
selected.update(
selected.value
.toMutableSet()
.apply { if (isSelected(item)) remove(item) else add(item) }
.toSet())
}
val selectAllClickCallback = useCallback { _: String ->
selected.update(if (isAllSelected()) emptySet() else items.toSet())
}
return LazyList {
val isAllSelected = isAllSelected()
child(deps = arrayOf(isAllSelected)) {
Selectable(
item = "Select All", selected = isAllSelected, onRowClick = selectAllClickCallback)
}
children(items = items, id = { it }) {
val selected = isSelected(it)
Selectable(item = it, selected = selected, onRowClick = selectItemClickCallback)
}
child(
Text(
selected.value.joinToString(prefix = "Selected: "),
textColor = Color.DKGRAY,
style = Style.padding(horizontal = 20.dp, vertical = 10.dp)))
}
}
}
The callback that will be invoked by the function returned by useCallback
is updated when a new layout has been committed on the main thread. This means that useCallback
should only be used with events that will also be invoked from the main thread, e.g. onClick.