State
A Litho component can contain two types of data:
- props: passed down from parent and cannot change during a component's lifecycle.
- state: encapsulated and managed within the component, and is transparent to the parent.
States for a given Component are the union of all arguments annotated with
State
in the spec. While both Prop and
State hold information that influences the output of the component, they are different in one
important way: props get passed to the component from its parent whereas states are managed
within the component.
The initial values of states can be set using the OnCreateInitialState method and states can be updated in OnUpdateState methods. Updating states in the OnUpdateState methods will cause the component to invoke its OnCreateLayout method. States should be immutable since the layout can be calculated on multiple threads. Immutability of the states ensures that no thread safety issues can occur in the component hierarchy.
A common example of State usage is rendering a checkbox. The component renders different drawables for the checked and unchecked states, but this is an internal detail of the checkbox component that the parent doesn't need to be aware of.
Declaring a Component State
You can define a State on a Component by using the @State annotation in the spec lifecycle methods, similarly to how you would define a Prop.
Defining state elements is enabled on the lifecycle methods of Layout Specs and Mount Specs.
Initializing a State value
To set an initial value for a state, you have to write a method annotated with @OnCreateInitialState
in your spec.
This is what you need to know when writing an @OnCreateInitialState
method:
- The first parameter must be of type
ComponentContext
. @Prop
parameters are allowed.- All other parameters must have a corresponding parameter annotated with
@State
in the other lifecycle methods, and their type must be a StateValue that is parameterized with the type of the matching@State
element. @OnCreateInitialState
methods are not mandatory. If you do not define one or if you only initialize some states, the uninitialized ones will take Java defaults.@OnCreateInitialState
is called only once for each component, when it first gets added to theComponentTree
. Following layout recalculations of the sameComponentTree
will not call this again if the key of the component doesn't change.- You should never need to call the
@OnCreateInitialState
method yourself.
Here's how you would initialize the checkbox state with a value passed down from the parent:
Defining State Updates
You can define how a component's state or states should be updated by declaring methods annotated with @OnUpdateState
in the specs.
You can have as many @OnUpdateState
methods as you need, according to what states you want to update or what parameters your states depend on.
Each call to an @OnUpdateState
method will trigger a new layout calculation for its ComponentTree. For better performance, if there are situations that can trigger an update for multiple states, you should define an @OnUpdateState
method that updates the value for all those states. Bundling them in the same update call reduces the number of new layout calculations and improves performance.
This is what you need to know when writing an @OnUpdateState
method:
- Parameters representing the states must match the name of a parameter annotated with
@State
and their type must be aStateValue
parameterized with the type of the matching@State
. @Param
parameters are allowed. If the value of your state depends on props, you can declare them like this and pass the value of the prop when the update call is triggered.- All other parameters must have a corresponding parameter annotated with
@State
in the other lifecycle methods, and their type must be aStateValue
parameterized with the type of the matching@State
element.
Here's how you would define a state update method for the checkbox:
If you want to bundle multiple state updates in a single method, you would just add all those states as parameters to the same @OnUpdateState
method:
Calling state updates
For each @OnUpdateState
method in your spec, the generated component will have two methods that will delegate to the @OnUpdateState
method under the hood:
- a static method with the same name, which will asynchronously apply the state updates.
- a static method with the same name and a Sync suffix, which will synchronously trigger the state updates.
Both methods take a
ComponentContext
as first parameter, followed by all the parameters declared with@Param
in your@OnUpdateState
method.
Here's how you would call the state update method to update your checkbox when a user clicks it:
This is what you need to keep in mind when calling state update methods:
- When calling a state update method, the
ComponentContext
instance passed as first parameter must always be the one that is passed down as parameter in the lifecycle method in which the update state is triggered. This context contains important information about the currently known state values and it's important for transferring these values from the old components to the new ones during new layout calculations. - In
LayoutSpec
s, you should avoid calling state update methods inonCreateLayout
, unless you are absolutely certain they will happen only a deterministic, small number of times. Every call to a state update method will trigger a new layout calculation on the ComponentTree, which in turn will callonCreateLayout
on all its components, so it's rather easy to go into an infinite loop. You should consider whether a lazy state update (described below) wouldn't be more appropriate for your use case. - In
MountSpec
s, you should never call update state methods frombind
andmount
methods. If you need to update a state value in those methods, you should instead use a lazy state update, described below. - State is a concept local to components. You cannot call a state update method from outside a component. Props are the mechanism to update a component based on outside changes. You can read more about that here.
Keys and identifying components
The framework sets a key on each component, based on its type and the key of its parent. This key is used to determine which component we want to update when calling a state update and finding this component when traversing the tree.
Components of the same type that have the same parent will be assigned the same key, so we need a way of uniquely identifying them.
Moreover, when a Component's state or props are updated and the ComponentTree
is recreated, there are situations when components are removed, added or rearranged inside the tree. Because components can be dynamic we need a way of keeping track of the components so we know, even after the ComponentTree
changes, for which component to apply a state update.
Whenever a key collision is detected in a ComponentTree, which can happen when a parent component created multiple children components of the same type, we assign a unique key on those siblings which depends on the order in which they added to the parent. However, with the current implementation, there's no easy way for us to detect that a component is the same when the order of the components in your hierarchy changes. This means that the keys that are autogenerated are not stable through component moves. If you expect your components to move around, you have to assign manual keys.
The Component.Builder
class exposes a .key()
method that you can call when creating a component to assign a unique key to it that will be used to identify this component.
You should set this key whenever you have multiple children of the same component with the same type or you expect the content of your layout to be dynamic.
The manual key you set on a component using the key
prop will always take precedence over the autogenerated one.
The most common case when you must manually define keys for your components is creating and adding them as children in a loop:
If a component with key A
updates its state, and later it is removed from the hierarchy and added back again with the same key A
, its state will be reset to the initial value. That means that an updated state value will only persist as long as the component it belongs to is part of the ComponentTree hierarchy.
Lazy State Updates
For situations where you want to update the value of a State
, but don't need to immediately trigger a new layout calculation, you can use lazy state updates. After a lazy state update, the new state value will be visible in event handlers, but a new layout will not be triggered. Currently, the value is not visible to other lifecycle callbacks (e.g. onMount
).
This is useful for updating state that doesn't need to be reflected in the UI. For example, say you want to log an analytics event only the first time a Component becomes visible. If you use lazy state, you can record whether a log was sent in a lazy @State
variable without causing the UI to reflow.
To use lazy state updates, you need to set the canUpdateLazily
parameter on the @State
annotation to true
.
For a state parameter foo
marked with canUpdateLazily
, the framework will generate a static state update method named lazyUpdateFoo
which takes a new value as parameter that will be set as the new value for foo
.
States marked as canUpdateLazily
can still be used for regular state updates.
Let's look at an example:
The first time FooComponent is rendered, its child Text
component will display "first foo", even if foo
is lazily updated with another value. When a regular state update or receiving new props will trigger a new layout calculation, the lazy state update will be applied and the Text
will render "updated foo".
Immutability
Because of background layout, State
can be accessed at anytime by multiple threads. To ensure thread safety, State
objects should be immutable (and if for some rare reason this is not possible, then at least thread safe). The simplest solution is to express your state in terms of primitives since primitives are by definition immutable.