Skip to main content

Component Lifecycle and Hooks

note

This page covers how to convert existing code from lifecycle methods in the Specs API to hooks in the Kotlin API.

For information on the concept and rules for hooks, see the Introduction to Hooks page.

One of the biggest changes in the Litho Kotlin API is the introduction of hooks: most of the APIs implemented with lifecycle methods in the Specs API (such as @OnCreateInitialState and @OnAttached) have hooks equivalents in the Kotlin API.

@State, @OnCreateInitialState and @OnUpdateState​

useState enables a component to persist a variable across renders and replaces the @State, @OnCreateInitialState, and @OnUpdateState APIs. The following code shows a comparison of Spec API and Kotlin API code for the same component:

class CheckboxComponent : KComponent() {
override fun ComponentScope.render(): Component {
val isChecked = useState { false }

return Column(style = Style.onClick { isChecked.update { currValue -> !currValue } }) {
child(
Image(
drawable =
drawableRes(
if (isChecked.value) {
android.R.drawable.checkbox_on_background
} else {
android.R.drawable.checkbox_off_background
})))
}
}
}

Side-effects in @OnCreateInitialState​

A common pattern in the Spec API was to perform side-effects in @OnCreateInitialState (such as attaching a listener). Instead, any side-effects like this should now be done with the useEffect hook, which provides functionality to clean up and handle prop changes.

For more information, see the useState page in the 'Main Concepts' section.

@OnAttached/@OnDetached​

The useEffect hook gives the ability to safely perform side-effects from a component and corresponds to the Spec API's @OnAttached/@OnDetached lifecycle methods.

useEffect(username) {
println("I've been attached with prop $username!")
onCleanup { println("I've been detached with prop $username!") }
}

In addition to the existing @OnAttached/@OnDetached functionality, it provides the ability to perform side-effects in response to changes in committed props or state. Both the effect lambda and the cleanup lambda are invoked on the main thread, meaning it's safe to perform UI-thread confined side-effects.

Changes to Attach/Detach Contract​

In the Spec API, @OnAttached/@OnDetached would be called only once, when the component was first attached to/detached from the tree. This could lead to bugs where, for example, when a component subscribes to a data store using a userId prop, and then that prop changes.

useEffect tries to solve this by taking a var-arg list of dependencies: any time any of these dependencies changes, the cleanup lambda (previously @OnDetached) will be called followed by the new effect lambda. If no dependencies are provided, the previous cleanup and new effect lambdas will be invoked again every time a new layout is committed.

Listening to Prop/State Changes​

An important functionality that useEffect adds over @OnAttached/@OnDetached is the ability to trigger code when props/state change. For example, this can be used to trigger an animation as a side-effect whenever a 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)
}
}

For more information, see the useEffect page in the 'Main Concepts' section.

Lazy State​

The useRef hook allows a component to maintain a mutable reference that doesn't trigger a re-render when updated.

It returns an instance of Ref that has a single mutable value property, which should only be read/written on the UI thread. useRef can be used as a replacement for lazy state from the Spec API, though it has additional uses.

Example: logging 'seen' state​

class LogOnceComponent : KComponent() {
override fun ComponentScope.render(): Component {
val hasLoggedVisible = useRef<Boolean> { false }

return Text(
style =
Style.onVisible {
// onVisible executes on the main thread, so it's safe to read/write hasLoggedVisible
if (!hasLoggedVisible.value) {
doLogVisible(androidContext)
hasLoggedVisible.value = true
}
},
text = "I'll let you know when I'm visible, but only once!")
}
}

For more information, see the useRef page in the 'Main Concepts' section.

@OnError​

The useErrorBoundary hook allows KComponents to catch and handle errors higher up in the tree and provide appropriate fallback, logging or retry mechanisms. It corresponds to the Spec API's @OnError functionality.

Example: providing an 'error' state in the UI​

A KComponent becomes an error boundary when it declares a useErrorBoundary hook. The example below shows how the implementation of a boundary that renders an error state compares between the Spec API and the Kotlin API:

class KErrorBoundary(private val childComponent: Component) : KComponent() {

override fun ComponentScope.render(): Component? {
val errorState = useState<Exception?> { null }
useErrorBoundary { exception: Exception -> errorState.update(exception) }

errorState.value?.let {
return Column(style = Style.margin(all = 16.dp)) {
child(KDebugComponent(message = "KComponent's Error Boundary", throwable = it))
}
}

return childComponent
}
}

For more information, see the useErrorBoundary page in the 'Main Concepts' section.

@OnCreateTransition​

useTransition registers a transition to be applied when the current layout is committed. It corresponds to the Spec API's @OnCreateTransition functionality.

class TransitionComponent : KComponent() {
override fun ComponentScope.render(): Component {
val isHalfAlpha = useState { false }
useTransition(Transition.create("square").animate(AnimatedProperties.ALPHA))

return Column(style = Style.onClick { isHalfAlpha.update(!isHalfAlpha.value) }) {
child(
Row(
style =
Style.backgroundColor(Color.YELLOW)
.width(80.dp)
.height(80.dp)
.alpha(
if (isHalfAlpha.value) {
0.5f
} else {
1.0f
})
.transitionKey(context, "square")))
}
}
}