@State Macro: Component Internal State
Note:
Currently in the beta phase.
A variable decorated with @State, also known as a state variable, can trigger UI component refreshes once it possesses the state attribute. When the state changes, the UI will undergo corresponding rendering updates.
Among state variable-related macros, @State is the most fundamental macro that endows variables with state attributes, serving as the data source for most state variables.
Before reading the @State documentation, developers are advised to have a basic understanding of the state management framework. It is recommended to read State Management Overview in advance.
Overview
Variables decorated with @State, like other decorated variables in the declarative paradigm, are private and can only be accessed from within the component. They must specify their type and local initialization upon declaration. Initialization can also be completed using the named parameter mechanism from the parent component.
Variables decorated with @State have the following characteristics:
-
A one-way data synchronization is established between @State-decorated variables and @Prop-decorated variables in child components, while a two-way data synchronization is established with @Link-decorated variables.
-
The lifecycle of @State-decorated variables is the same as that of their custom component.
Macro Usage Rules Explanation
| @State | Description |
|---|---|
| Non-attribute macro | None. |
| Synchronization type | Does not synchronize with any type of variable in the parent component. |
| Allowed variable types for decoration | Supports basic data types. For String, Int64, Float64, and Bool types, the type can be omitted. Other types must be explicitly specified. Supports Enum, Option types, and struct types, but internal modifications within struct types are not allowed. Supports class types. To observe internal changes, the class must be decorated with @Observed at definition time. Class properties and nested properties must be decorated with @Publish to observe changes. Supports array types. To observe internal changes, use ObservedArrayList<T>. For custom array items, use @Observed and @Publish to observe property assignments within array items. Other array and Collection types, such as Array, Varray, ArrayList, HashMap, and HashSet, support assignment of new arrays but cannot observe changes to internal elements. Supports the Color type. For supported type scenarios, see Observing Changes. Does not support Any. |
| Not recommended decorated variable types | Not recommended to decorate function types. |
| Initial value of decorated variables | Must be initialized locally. |
Variable Passing/Access Rules Explanation
| Passing/Access | Description |
|---|---|
| Initialization from parent component | Optional, can be initialized from the parent component or locally. If initialized from the parent component, the passed value will override local initialization. Supports initialization of child component @State with regular variables (assigning regular variables to @State only initializes the value; changes to regular variables do not trigger UI refreshes; only state variables can trigger UI refreshes), @State, @Link, @Prop, @Provide, @Consume, @StorageLink, and @StorageProp decorated variables from the parent component. |
| Used to initialize child components | @State-decorated variables support initializing child component regular variables, @State, @Link, @Prop, and @Provide. |
| Supports external component access | No, can only be accessed within the component. |
Observing Changes and Behavior
Not all changes to state variables will cause UI refreshes; only modifications observable by the framework will trigger UI updates. This section explains what modifications can be observed and how the framework triggers UI refreshes upon observing changes, i.e., the framework's behavior.
Observing Changes
-
When the decorated data type is a basic type, numerical changes can be observed.
// Simple type @State var count: Int = 0 // Value changes can be observed this.count = 1 -
When the decorated data type is a struct, internal modifications are not allowed.
Declare Person.
struct Person { var id: Int64 var name: String public init(id: Int64, name: String) { this.id = id this.name = name } }@State decorates a struct Person type.
// Struct type @State var person: Person = Person(1, "Kim")Overall assignment to @State-decorated variables is allowed.
// Struct type assignment this.person = Person(2, "muller")Assignment to @State-decorated variables shows compiler errors for modifications.
// Struct property assignment this.person.id = 3 -
When the decorated data type is a class, it must be decorated with @Observed, and properties requiring change observation must be decorated with @Publish. Without @Observed, internal changes like member variables cannot be observed. See @Observed Macro and @Publish Macro for details.
Declare Person and Model classes.
@Observed class Person { @Publish var value: String } @Observed class Model { @Publish var value: String = "" @Publish var name: Person = Person(value: " ") }@State decorates a Model type.
@State var title: Model = Model(value: 'Hello', name: Person(value: "World"))Assignment to @State-decorated variables.
// Class property assignment this.title = Model(value: 'Hi', name: Person(value: 'ArkUI'))Assignments to properties and nested properties of @State-decorated variables can be observed.
// Class property assignment this.title.value = 'Hi' // Nested property this.title.name.value = 'ArkUI' -
When the decorated object is an array, individual array item changes cannot be observed, but overall changes can. To observe internal changes, use ObservedArrayList<T>.
When @State decorates an ArrayList type array.
@State var arrlist: ArrayList<Int16> = ArrayList<Int16>([1, 2, 3])Overall array changes can be observed.
this.arrlist = ArrayList<Int16>([10,9,8])Array item assignments cannot be observed.
this.arrlist[0] = 10Declare Model class.
@Observed class Model { @Publish public var value: Int }When @State decorates an ObservedArray<Model> type array.
// ObservedArray array type @State var title: ObservedArrayList<Model> = ObservedArrayList<Model>(ArrayList<Model>([Model(value: 11), Model(value: 1)]))Array self-assignments can be observed.
// Array assignment this.title = ObservedArrayList<Model>(ArrayList<Model>([Model(value: 2)]))Array item assignments can be observed.
// Array item assignment this.title[0] = Model(value: 2)Property assignments within array items can be observed.
// Nested property assignments can be observed this.title[0].value = 6When @State decorates an ObservedArrayList<Model> type array, implement adding and deleting array items.
Deleting array items can be observed.
// Array item change this.title.remove(0)Adding array items can be observed.
// Array item change this.title.append(Model(value: 12)) -
When the decorated variable is other array or Collection types, such as Array, Varray, ArrayList, HashMap, and HashSet, assigning new arrays is supported, but internal element changes cannot be observed.
Using HashSet as an example.
// When \@State decorates a HashSet @State var message: HashSet<Int64> = HashSet<Int64>()Overall HashSet changes can be observed.
this.message = HashSet<Int64>(1,2,3)Internal HashSet changes cannot be observed.
this.message.add(5);
Framework Behavior
-
When a state variable changes, components dependent on that state variable are queried;
-
The update method of components dependent on the state variable is executed, and the component updates its rendering;
-
Components or UI descriptions unrelated to the state variable will not re-render, achieving on-demand page rendering updates.
Constraints
-
Variables decorated with @State must be initialized; otherwise, a compilation error will occur.
// Incorrect, compilation error @State var count: Int // Correct @State var count: Int = 0 -
@State does not support decorating Function-type variables; a compilation error will occur.
Usage Scenarios
Decorating Simple Type Variables
The following example shows a simple type decorated with @State. count becomes a state variable when decorated with @State, and changes to count trigger Button component refreshes:
-
When the state variable count changes, only the Button component associated with it is queried;
-
The Button component's update method is executed, achieving on-demand refreshes.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Entry
@Component
class EntryView {
@State var count: Int = 0
func build() {
Button("click times: ${count}").onClick({evt => this.count += 1})
}
}

Decorating Class Object Type Variables
-
The custom component MyComponent defines state variables count and title decorated with @State, where title is of the custom class Model. If the values of count or title change, UI components using these state variables within MyComponent are queried and re-rendered.
-
EntryView contains multiple MyComponent instances. Changes to the internal state of the first MyComponent do not affect the second MyComponent.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Observed
class Model {
@Publish public var value: String
}
@Entry
@Component
class EntryView {
func build() {
Column {
// Parameters specified here will override locally defined default values during initial rendering; not all parameters need initialization from the parent component
MyComponent(count: 1, increaseBy: 2)
MyComponent(title: Model(value: 'Hello World 2'), count: 7)
}
}
}
@Component
class MyComponent {
@State var title: Model = Model(value: 'Hello World')
@State var count: Int64 = 0
private var increaseBy: Int64 = 1
func build() {
Column {
Text("${this.title.value}").margin(10)
Button("Click to change title")
.onClick({ // Updates to @State variables will trigger the above Text component content update
evt => this.title.value = 'Hello ArkUI'
})
.width(300)
.margin(10)
Button("Click to increase count = ${this.count}")
.onClick({
// Updates to @State variables will trigger this Button component content update
evt => this.count += this.increaseBy
})
.width(300)
.margin(10)
}
}
}

From this example, the initialization mechanism of @State variables can be understood:
-
Without external input, default values are used for initialization:
// title has no external input; uses the local value Model(value: 'Hello World') for initialization MyComponent(count: 1, increaseBy: 2) // increaseBy has no external input; uses the local value 1 for initialization MyComponent(title: Model(value: 'Hello World 2'), count: 7) -
With external input, external values are used for initialization:
// count and increaseBy have external input; uses passed values 1 and 2 for initialization MyComponent(count: 1, increaseBy: 2) // title and count have external input; uses passed values Model(value: 'Hello World 2') and 7 for initialization MyComponent(title: Model(value: 'Hello World 2'), count: 7)
Common Issues
Arrow Function Changes to State Variables Not Taking Effect
The this object within an arrow function refers to the object in the scope where the function is defined, not where it is used. Therefore, the current this.vm must be passed in to call the property assignment of the proxied state variable.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Observed
class PlayDetailViewModel {
@Publish var coverUrl: UInt32
var changeCoverUrl: (PlayDetailViewModel) -> Unit
}
@Entry
@Component
class EntryView {
@State var vm: PlayDetailViewModel = PlayDetailViewModel(coverUrl: 0x00ff00,
changeCoverUrl: {model: PlayDetailViewModel => model.coverUrl = 0x00F5FF})
func build() {
Column() {
Text(this.vm.coverUrl.toString())
.width(100)
.height(100)
.backgroundColor(this.vm.coverUrl)
Button('Click to change color').onClick(
{
evt =>
let self = this.vm
this.vm.changeCoverUrl(self)
}
)
}
}
}

State Variables Only Affect UI Components They Directly Bind To
【Example 1】
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Observed
class Info {
@Publish var address: String = 'Hangzhou'
}
@Entry
@Component
class EntryView {
@State var message: String = 'Shanghai'
@State var info: Info = Info()
public func aboutToAppear() {
this.info.address = this.message
}
func build() {
Column() {
Text(this.message)
Text(this.info.address)
Button('change').onClick({
evt => this.info.address = 'Beijing'
})
}
}
}

In the above example, clicking Button('change') only triggers a refresh of the second Text component because message is a simple string type, which is value-copied. Thus, clicking the button changes the address value in info without affecting this.message.
【Example 2】
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Observed
class Info {
@Publish var address: String = 'Hangzhou'
}
@Observed
class User {
@Publish var infomation: Info
}
@Entry
@Component
class EntryView {
@State var info: Info = Info(address: 'Shanghai')
@State var user: User = User(infomation: Info(address: 'Tianjin'))
public func aboutToAppear() {
this.user.infomation = this.info
}
func build() {
Column() {
Text(this.info.address)
Text(this.user.infomation.address)
Button('change').onClick(
{
evt =>
this.user.infomation = Info(address: 'Guangzhou')
this.user.infomation.address = 'Beijing'
}
)
}
}
}

In the above example, clicking Button('change') only triggers a refresh of the second Text component. This is because clicking the button first executes this.user.infomation = Info(address: 'Guangzhou'), creating a new Info object. Then, this.user.infomation.address = 'Beijing' changes the address value of this newly created Info object, leaving the original Info object's address value unaffected.### Prohibition of Modifying State Variables within Build
Modifying state variables within the build method is strictly prohibited. State management frameworks will log an Error-level message during runtime.
The rendering process in the following example is as follows:
- Create the custom
Indexcomponent. - Execute the
buildmethod ofIndex: a. Create theColumncomponent. b. Create theTextcomponent. During the creation of theTextcomponent,this.count++is triggered. c. The change incounttriggers a refresh of theTextcomponent again. d. TheTextcomponent is finally rendered.
@Entry
@Component
class EntryView {
@State var count :Int64 = 1
func build() {
Column() {
// Avoid directly modifying the value of count within the Text component
Text("${this.count++}")
.width(50)
.height(50)
}
}
}
In the above example, this erroneous behavior does not cause severe consequences. However, it is fundamentally incorrect and will lead to increasingly significant risks as project complexity grows. See the next example.
@Entry
@Component
class EntryView {
@State var message: Int = 20;
func build() {
Column() {
Text("${this.message++}")
Text("${this.message++}")
}
.width(50)
.height(100)
}
}
The rendering process in the above example is as follows:
- Create the first
Textcomponent, triggering a change inthis.message. - The change in
this.messagetriggers a refresh of the secondTextcomponent. - The refresh of the second
Textcomponent triggers another change inthis.message, which in turn triggers a refresh of the firstTextcomponent. - This creates a loop of repeated rendering.
- The system becomes unresponsive for an extended period, resulting in app freeze.
Therefore, modifying state variables within the build method is entirely incorrect.
Deregistration Required When Modifying State Variables via Callback Registration
Developers can register arrow functions in onPageShow to modify state variables within components. However, it is crucial to set the previously registered functions to null in aboutToDisappear. Otherwise, the arrow function capturing the this instance of the custom component will prevent the component from being released, leading to memory leaks.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Observed
class Model {
@Publish var callback: () -> Unit
func add(callback: () -> Unit) {
this.callback = callback
}
func delete() {
this.callback = Option<() -> Unit>.None.getOrThrow()
}
func call() {
if (true) {
this.callback()
}
}
}
let model = Model(callback: {=>})
@Entry
@Component
class EntryView {
@State var count: Int64 = 10
public override func onPageShow() {
model.add({=> this.count++})
}
func build() {
Column() {
Text("count value: ${this.count}")
Button('change').onClick({
evt => model.call()
})
}
}
public func aboutToDisappear(){
model.delete()
}
}
Additionally, state variables can also be modified outside the custom component using the LocalStorage approach.