useEffect
useEffect
is a hook that allows a component to perform side-effects1 when it's attached and/or detached from the tree, or in response to changes in committed props or state.
To familiarize yourself with the concept and rules for hooks, see the Introduction to Hooks page.
Performing side-effectsβ
useEffect
lambdas and cleanup are both invoked on the main thread. This means it's safe to trigger animations or mutate the view hierarchy from it. However, it's recommended to avoid expensive operations in order to keep your app responsive!
useEffect
accepts an effect lambda to invoke when the component is attached to the tree. Besides performing a side-effect, like subscribing to a data store, the effect lambda should also return a cleanup lambda, which is invoked when the component no longer exists in the tree.
useEffect(Unit) {
println("I've been attached!")
onCleanup { println("I've been detached!") }
}
The arguments to useEffect
are its dependencies, which control when the effect is cleaned up and invoked again. With Unit
passed as a dependency arg, the effect lambda is invoked only once when a component is added to UI tree, and the cleanup lambda is invoked only once when a component is removed from the UI tree. They won't be called when a component is updated due to the props or state changes.
You can have a finer-grained control of how often your effect and cleanup lambdas need to be executed using dependency args, as shown in the next section.
Side-effects with dependenciesβ
Dependencies are checked for equivalence by calling equals
. If any are not equal, the hook will be cleaned up and called again.
In addition to the effect lambda useEffect
also accepts a var-arg list of dependencies. These dependencies are the props and/or state which affect the side-effect performed. When dependencies are supplied, the effect lambda (along with the previous cleanup lambda, if applicable) will only be invoked on initial attach and any commit in which any of those dependencies have changed.
In the sample code below, when the first render is committed, Litho will invoke the useEffect
lambda, subscribing to the current userId
. On subsequent commits, if userId
or store
changed, Litho invokes the cleanup callback to unsubscribe the existing subscription (if there is one) and then subscribe to the current userId
and store
.
class UserStatusComponent(private val userId: Int, private val store: StatusStore) : KComponent() {
override fun ComponentScope.render(): Component {
val status = useState { Status.PENDING }
val subscription = useRef<Subscription?> { null }
useEffect(userId, store) {
subscription.value = store.subscribe(userId) { newStatus -> status.update(newStatus) }
onCleanup { store.unsubscribe(subscription.value) }
}
return Text(text = "$userId is ${status.value}")
}
}
In general, your dependencies should include all the props/state read by you useEffect
/onCleanup
calls. If they don't, you risk incorrect behavior. For example, in the above component, if userId
hadn't been specified, then if the component received a different userId
as a prop, it would remain subscribed to the wrong userId
.
Cleanupβ
The return value of the lambda passed to useEffect
is a cleanup lambda. This is where you should perform any cleanup necessary to undo side-effects from the main body of that useEffect
call. For example, canceling a subscription to a data store, or canceling an animation. If you don't have any cleanup you need to do, you can return null
instead:
useEffect {
Toast.makeText(androidContext, "Component rendered", Toast.LENGTH_SHORT).show()
null // return null, no cleanup necessary
}
Unconditionally triggering side-effectsβ
In rare cases you may need to pass Any()
as a dependency arg. In such a case, on each new commit, the cleanup for the previous useEffect
call is invoked, followed by the current useEffect
call.
Listening to Prop/State Changesβ
An important functionality that useEffect
adds is the ability to trigger code when props/state change. As an example, this could be used to trigger an animation as a side-effect whenever some prop changes:
class AnimatingCounter(private val count: Int) : KComponent() {
override fun ComponentScope.render(): Component? {
val translationY = useBinding(0f)
useEffect(count) {
// Animate the text to a Y-offset based on count
val animation = Animated.spring(translationY, to = count * 10.dp.toPixels().toFloat())
animation.start()
onCleanup { animation.cancel() }
}
return Text(style = Style.translationY(translationY), text = "$count", textSize = 24.sp)
}
}
Known issuesβ
useEffect
in Components that can return null
or EmptyComponent
from render
β
If you specify an effect via useEffect
and then return null
from render
, or render to a child which returns null
from render (like EmptyComponent
), then the effect won't run.
Until this is fixed, the workaround is to return an empty Row()
from render instead of null.
- Side-effects include operations such as fetch requests, subscriptions, using timers, and manually changing the View tree. By using the useEffect hook, you are stating that the component needs to do something after its rendered.β©