DEV Community

HarmonyOS
HarmonyOS

Posted on

How to Solve the Issue of Page Not Refreshing When Using @State to Decorate Object Arrays and Data Changes

Read the original article:How to Solve the Issue of Page Not Refreshing When Using @State to Decorate Object Arrays and Data Changes

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?

cke_3220.png After Click Modify Still same cke_4184.png

cke_5210.png After Click Replace It refreshed cke_6252.png

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%')
  }
}
Enter fullscreen mode Exit fullscreen mode

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 })
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. 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.
  2. 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:
  1. Use @Observed to annotate the class:

     @Observed
      class Person {
        name:string;
        age:number;
    
        constructor(name:string, age:number) {
          this.name= name;
          this.age= age;
        }
      }
    
  2. 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)
         }
       }
     }
    
  3. 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:
  1. 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[]
     }
    
  2. 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())
             })
         }
       }
     }
    
  3. 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

Written by Dogan Evci

Top comments (0)