Running Side Effects & Cleanup
Signature: fun ComponentScope.useEffect(vararg deps: Any?, effect: () -> Unit)
| Dependency Pattern | When Effect Runs | When Cleanup Runs |
|---|---|---|
useEffect(Unit) { ... } | Once on attach | Once on detach |
useEffect(dep1, dep2) { ... } | On attach + when deps change | Before re-run + on detach |
useEffect(Any()) { ... } | On every render | Before each re-run + on detach |
Cleanup: Return onCleanup { ... } from effect lambda, or null if no cleanup needed.
Import:
import com.facebook.litho.useEffect
import com.facebook.litho.onCleanup
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.
import com.facebook.litho.useEffect
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.
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.useEffect
import com.facebook.litho.useRef
import com.facebook.litho.useState
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:
import com.facebook.litho.useEffect
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.
Common Patterns
Include all props/state read by your effect in the dependency list:
// ✅ Correct - all used values are dependencies
useEffect(userId, store) {
store.subscribe(userId) { ... }
onCleanup { store.unsubscribe() }
}
Don't omit dependencies - may cause stale closures:
// ❌ Incorrect - userId changes won't re-subscribe
useEffect(store) { // Missing userId!
store.subscribe(userId) { ... }
}
Always return cleanup for subscriptions, timers, or listeners:
// ✅ Correct - cleanup prevents memory leaks
useEffect(Unit) {
val subscription = eventBus.subscribe { ... }
onCleanup { eventBus.unsubscribe(subscription) }
}
Don't forget to clean up subscriptions, timers, or listeners:
// ❌ Incorrect - memory leak! Subscription never unsubscribed
useEffect(Unit) {
eventBus.subscribe { ... }
null // No cleanup!
}
See Also
- Introduction to Hooks - Hook fundamentals and rules
- useState - Persist values across renders
- useRef - Store mutable references from effects
- useCached - Cache expensive calculations
- 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.↩