Skip to main content

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​

note

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​

note

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.


  1. 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.↩