Problem Description
When using the @State modifier on an object array and directly modifying the properties of the objects within it, the system fails to detect changes in the properties of the objects within the array, resulting in the inability to render and refresh the page. As observed in the image below, when we directly modify the properties of objects in the object array, although the values are successfully changed, the page does not refresh. How can we achieve synchronous UI re-rendering when modifying object properties?
After Click Replace It refreshed 
Problem Scenario One: After decorating an object array with @State, since @State can only observe changes at the first level of data, which is the changes in each item within the array, changes in the properties of elements within the items of the array cannot be observed.
class PersonPre {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Entry
@Component
struct Index2 {
@State persons: PersonPre[] = [
new PersonPre('Xiao Zhang', 12),
new PersonPre('Xiao Zhang', 13),
new PersonPre('Xiao Zhang', 14),
new PersonPre('Xiao Zhang', 15),
]
build() {
Scroll() {
Column({space: 10}) {
ForEach(this.persons, (curPerson: PersonPre, index: number) => {
Column() {
Text(`Name${curPerson.name}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(`Age${curPerson.age}`)
.fontSize(15)
.fontWeight(FontWeight.Regular)
}
})
Button('Modify the object array property - change Xiao Zhang\'s age to 66 ')
.onClick(() => {
this.persons[0].age = 66
console.info('Currently, Xiao Zhang\'s age is ' + JSON.stringify(this.persons[0]))
})
Button('Replace the element in the object array - change Xiao Zhang\'s age to 88. ')
.onClick(() => {
this.persons.splice(0, 1, new PersonPre('Xiao Zhang', 88))
console.info('Currently, Xiao Zhang\'s age is' + JSON.stringify(this.persons[0]))
})
}
}
.height('100%')
.width('100%')
}
}
Problem Scenario Two: The @ObjectLink decorator cannot be used within custom components decorated with @Entry. Instead, it is necessary to use child components in conjunction with @ObjectLink to observe changes in deeply nested object properties.
@Observed
class perInfo {
name: string
age: number
friends: perInfo[]
constructor(model: perInfoModel) {
this.name = model.name;
this.age = model.age;
this.friends = model.friends.map(friend => new perInfo(friend));
}
}
interface perInfoModel {
name: string
age: number
friends: perInfoModel[]
}
@Entry
@Component
struct Index {
@ObjectLink info: perInfo
@State infos: perInfo = new perInfo({
name: 'Tom',
age: 18,
friends: [
{
name: 'Jerry',
age: 18,
friends: [{ name: "Tom", age: 20, friends: [] }]
},
{
name: 'Mike',
age: 18,
friends: [{ name: "Nancy", age: 20, friends: [] }]
}
]
})
build() {
Column({ space: 15 }) {
Text('This is a page')
.fontSize(20)
Text(this.info.name)
.fontSize(16)
.fontColor(Color.Red)
.onClick(() => {
this.info.name += '!'
})
.backgroundColor(Color.Pink)
Text(this.info.age.toString())
.fontSize(16)
.fontColor(Color.Red)
.onClick(() => {
this.info.age++
})
.backgroundColor(Color.Pink)
ForEach(this.info.friends, (item: perInfo) => {
Column() {
Button(`${item.name}`)
.onClick(() => {
item.name += '!'
console.info(item.name)
})
Button(`${item.age}`)
.onClick(() => {
item.age++
console.info(item.age.toString())
})
}
})
}
.padding({ top: 30, right: 20, left: 20 })
}
}
Background Knowledge
The ForEach interface is designed for loop rendering based on array-type data and must be used in conjunction with container components. The components returned by the interface should be sub-components that are allowed to be included in the parent container component of ForEach. When the ForEach component performs non-first rendering, it matches based on the passed key values. If no matching key value is found, a new component is created. If the key value already exists, a new component is not created; instead, the component corresponding to that key value is directly rendered.
Variables decorated with @State, also known as state variables, can trigger the refresh of UI components directly bound to them once they possess state attributes. When the state changes, the UI undergoes corresponding rendering changes. The combination of @Observed/@ObjectLink is used for nested scene observation, primarily to compensate for the decorator's limitation of only observing one layer. It can be used to observe two-dimensional arrays, class attributes of array items, and classes where the attributes are classes, capturing changes in these second-layer properties.
Troubleshooting Process
- The @State decorator can only observe changes at the first level. However, in practical application development, applications often encapsulate their own data models based on development needs. For multi-level nesting scenarios, such as two-dimensional arrays, arrays of objects, or objects nested within objects, changes to properties at the second level cannot be observed. For example, when decorating an object array, you can observe the assignment, addition, deletion, and updates to the array itself, but changes to the properties within the array items cannot be observed.
- To achieve dynamic rendering, you need to use the @Observed/@ObjectLink decorator:
- @Observed is used in scenarios involving object arrays, array objects, and nested objects to observe changes in class properties and is used to annotate classes.
- The state variable decorated with the @ObjectLink decorator is used to receive instances of classes decorated with @Observed, establishing a two-way data binding with the corresponding state variable in the parent component. Note: The @ObjectLink decorator cannot be used in custom components decorated with @Entry.
Analysis Conclusion
Variables decorated with @State can observe changes in simple type variable values. In state management V1, to observe changes in a certain property of an object type within a class, the @Observed/@ObjectLink decorators need to be used.
Solution
- Suggestions for revising scenario one:
-
Use @Observed to annotate the class:
@Observed class Person { name:string; age:number; constructor(name:string, age:number) { this.name= name; this.age= age; } } -
Custom sub-components, Note that the @ObjectLink decorator cannot be used in custom components decorated with @Entry:
@Component struct MyPerson { @ObjectLink person: Person build() { Column() { Text(`Name${this.person.name}`) .fontSize(20) .fontWeight(FontWeight.Bold) Text(`Age${this.person.age}`) .fontSize(15) .fontWeight(FontWeight.Regular) } } } -
Using a child component in a parent component:
@Entry @Component struct PrePage { @State persons: Person[] = [ new Person('Xiao Zhang ', 12), new Person('Xiao Zhang ', 13), new Person('Xiao Zhang ', 14), new Person('Xiao Zhang ', 15), ] build() { Scroll() { Column({space: 10}) { ForEach(this.persons, (curPerson: Person, index: number) => { MyPerson({person: curPerson}) }) Button('Modify the object array property - change Xiao Zhang's age to 66') .onClick(() => { this.persons[0].age = 66 console.info('Currently, Xiao Zhang's age is' + JSON.stringify(this.persons[0])) }) Button('Replace the element in the object array - change Xiao Zhang's age to 88.') .onClick(() => { this.persons.splice(0, 1, new Person('Xiao Zhang', 88)) console.info('Currently, Xiao Zhang's age is' + JSON.stringify(this.persons[0])) }) } } .height('100%') .width('100%') } }
- Suggestions for revising scenario two:
-
Use @Observed to annotate the class:
@Observed class perInfo { name: string age: number friends: perInfo[] constructor(model: perInfoModel) { this.name = model.name; this.age = model.age; this.friends = model.friends.map(friend => new perInfo(friend)); } } interface perInfoModel { name: string age: number friends: perInfoModel[] } -
Customize the sub-components Child and Child2, noting that the @ObjectLink decorator cannot be used in custom components decorated with @Entry:
@Component struct Child { @ObjectLink info: perInfo build() { Column({ space: 10 }) { Text('This is a page') .fontSize(20) Text(this.info.name) .fontSize(16) .fontColor(Color.Red) .onClick(() => { this.info.name += '!' }) .backgroundColor(Color.Pink) Text(this.info.age.toString()) .fontSize(16) .fontColor(Color.Red) .onClick(() => { this.info.age++ }) .backgroundColor(Color.Pink) ForEach(this.info.friends, (item: perInfo) => { child2({ info: item }) }) } } } @Component struct child2 { @ObjectLink info: perInfo build() { Column({ space: 10 }) { Button(`${this.info.name}`) .onClick(() => { this.info.name += '!' console.info(this.info.name) }) Button(`${this.info.age}`) .onClick(() => { this.info.age++ console.info(this.info.age.toString()) }) } } } -
Using a child component in a parent component:
@Entry @Component struct Index { @State infos: perInfo = new perInfo({ name: 'Tom', age: 18, friends: [ { name: 'Jerry', age: 18, friends: [{ name: "Tom", age: 20, friends: [] }] }, { name: 'Mike', age: 18, friends: [{ name: "Nancy", age: 20, friends: [] }] } ] }) build() { Column({ space: 15 }) { Child({ info: this.infos }) } .padding({ top: 30, right: 20, left: 20 }) } }
Key Takeaways
- The @State decorator can only observe changes at the first level.
- For multi-level nested structures, such as arrays of objects, changes to their second-level properties cannot be observed.
- Classes decorated with @Observed can observe changes to their properties; state variables decorated with @ObjectLink are used to receive instances of classes decorated with @Observed, establishing a connection with the corresponding state variables in the parent component.
Related Documents or Links
https://developer.huawei.com/consumer/cn/forum/topic/0201191007454728759?fid=0109140870620153026


Top comments (0)