1# \@Observed and \@ObjectLink Decorators: Observing Attribute Changes in Nested Class Objects 2 3 4The aforementioned decorators can observe only the changes of the first layer. However, in real-world application development, an application may encapsulate its own data model. In this case, for multi-layer nesting, for example, a two-dimensional array, an array item class, or a class inside another class as an attribute, the attribute changes at the second layer cannot be observed. This is where the \@Observed and \@ObjectLink decorators come in handy. 5 6 7> **NOTE** 8> 9> These two decorators can be used in ArkTS widgets since API version 9. 10> 11> These two decorators can be used in atomic services since API version 11. 12 13## Overview 14 15\@ObjectLink and \@Observed class decorators are used for two-way data synchronization in scenarios involving nested objects or arrays: 16 17- Regarding classes decorated by \@Observed, the attribute changes can be observed. 18 19- The \@ObjectLink decorated state variable in the child component is used to accept the instance of the \@Observed decorated class and establish two-way data binding with the corresponding state variable in the parent component. The instance can be an \@Observed decorated item in the array or an \@Observed decorated attribute in the class object. 20 21- Using \@Observed alone has no effect in nested classes. To observe changes of class properties, it must be used with a custom component. (For examples, see [Nested Object](#nested-object)). To create a two-way or one-way synchronization, it must be used with \@ObjectLink or with \@Prop. (For examples, see [Differences Between \@Prop and \@ObjectLink](#differences-between-prop-and-objectlink).) 22 23 24## Constraints 25 26- Using \@Observed to decorate a class changes the original prototype chain of the class. Using \@Observed and other class decorators to decorate the same class may cause problems. 27 28- The \@ObjectLink decorator cannot be used in custom components decorated by \@Entry. 29 30 31## Decorator Description 32 33| \@Observed Decorator| Description | 34| -------------- | --------------------------------- | 35| Decorator parameters | None. | 36| Class decorator | Decorates a class. You must use **new** to create a class object before defining the class.| 37 38| \@ObjectLink Decorator| Description | 39| ----------------- | ---------------------------------------- | 40| Decorator parameters | None. | 41| Allowed variable types | Objects of \@Observed decorated classes. The type must be specified.<br>Simple type variables are not supported. Use [\@Prop](arkts-prop.md) instead.<br>Objects of classes that extend Date, [Array](#two-dimensional-array), [Map](#extended-map-class), and [Set](#extended-set-class) (the latter two are supported since API version 11). For an example, see [Observed Changes](#observed-changes).<br>(Applicable to API version 11 or later) Union type of @Observed decorated classes and **undefined** or **null**, for example, **ClassA \| ClassB**, **ClassA \| undefined** or **ClassA \| null**. For details, see [Union Type @ObjectLink](#union-type-objectlink).<br>An \@ObjectLink decorated variable accepts changes to its attributes, but assignment is not allowed. In other words, an \@ObjectLink decorated variable is read-only and cannot be changed. | 42| Initial value for the decorated variable | Not allowed. | 43 44Example of a read-only \@ObjectLink decorated variable: 45 46 47```ts 48// The \@ObjectLink decorated variable accepts changes to its attribute. 49this.objLink.a= ... 50// Value assignment is not allowed for the \@ObjectLink decorated variable. 51this.objLink= ... 52``` 53 54> **NOTE** 55> 56> Value assignment is not allowed for the \@ObjectLink decorated variable. To assign a value, use [@Prop](arkts-prop.md) instead. 57> 58> - \@Prop creates a one-way synchronization from the data source to the decorated variable. It takes a copy of its source to enable changes to remain local. When \@Prop observes a change to its source, the local value of the \@Prop decorated variable is overwritten. 59> 60> - \@ObjectLink creates a two-way synchronization between the data source and the decorated variable. An \@ObjectLink decorated variable can be considered as a pointer to the source object inside the parent component. Do not assign values to \@ObjectLink decorated variables, as doing so will interrupt the synchronization chain. \@ObjectLink decorated variables are initialized through data source (Object) references. Assigning a value to them is equivalent to updating the array items or class attributes in the parent component, which is not supported in TypeScript/JavaScript and will result in a runtime error. 61 62 63## Variable Transfer/Access Rules 64 65| \@ObjectLink Transfer/Access| Description | 66| ----------------- | ---------------------------------------- | 67| Initialization from the parent component | Mandatory.<br>To initialize an \@ObjectLink decorated variable, a variable in the parent component must meet all the following conditions:<br>- The variable type is an \@Observed decorated class.<br>- The initialized value must be an array item or a class attribute.<br>- The class or array of the synchronization source must be decorated by [\@State](./arkts-state.md), [\@Link](./arkts-link.md), [\@Provide](./arkts-provide-and-consume.md), [\@Consume](./arkts-provide-and-consume.md), or \@ObjectLink.<br>For an example where the synchronization source is an array item, see [Object Array](#object-array). For an example of the initialized class, see [Nested Object](#nested-object).| 68| Synchronization with the source | Two-way. | 69| Subnode initialization | Supported; can be used to initialize a regular variable or \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.| 70 71 72 **Figure 1** Initialization rule 73 74 75 76 77 78## Observed Changes and Behavior 79 80 81### Observed Changes 82 83If the attribute of an \@Observed decorated class is not of the simple type, such as class, object, or array, it must be decorated by \@Observed. Otherwise, the attribute changes cannot be observed. 84 85 86```ts 87class ClassA { 88 public c: number; 89 90 constructor(c: number) { 91 this.c = c; 92 } 93} 94 95@Observed 96class ClassB { 97 public a: ClassA; 98 public b: number; 99 100 constructor(a: ClassA, b: number) { 101 this.a = a; 102 this.b = b; 103 } 104} 105``` 106 107In the preceding example, **ClassB** is decorated by \@Observed, and the value changes of its member variables can be observed. In contrast, **ClassA** is not decorated by \@Observed, and therefore its attribute changes cannot be observed. 108 109 110```ts 111@ObjectLink b: ClassB 112 113// Value changes can be observed. 114this.b.a = new ClassA(5) 115this.b.b = 5 116 117// ClassA is not decorated by @Observed, and its attribute changes cannot be observed. 118this.b.a.c = 5 119``` 120 121\@ObjectLink: \@ObjectLink can only accept instances of classes decorated by \@Observed. When possible, design a separate custom component to render each array or object. In this case, an object array or nested object (which is an object whose property is an object) requires two custom components: one for rendering an external array/object, and the other for rendering a class object nested within the array/object. The following can be observed: 122 123- Value changes of the attributes that **Object.keys(observedObject)** returns. For details, see [Nested Object](#nested-object). 124 125- Replacement of array items for the data source of an array and changes of class attributes for the data source of a class. For details, see [Object Array](#object-array). 126 127For an instance of the class that extends **Date**, the value changes of **Date** attributes can be observed. In addition, you can call the following APIs to update **Date** attributes: **setFullYear**, **setMonth**, **setDate**, **setHours**, **setMinutes**, **setSeconds**, **setMilliseconds**, **setTime**, **setUTCFullYear**, **setUTCMonth**, **setUTCDate**, **setUTCHours**, **setUTCMinutes**, **setUTCSeconds**, and **setUTCMilliseconds**. 128 129```ts 130@Observed 131class DateClass extends Date { 132 constructor(args: number | string) { 133 super(args) 134 } 135} 136 137@Observed 138class ClassB { 139 public a: DateClass; 140 141 constructor(a: DateClass) { 142 this.a = a; 143 } 144} 145 146@Component 147struct ViewA { 148 label: string = 'date'; 149 @ObjectLink a: DateClass; 150 151 build() { 152 Column() { 153 Button(`child increase the day by 1`) 154 .onClick(() => { 155 this.a.setDate(this.a.getDate() + 1); 156 }) 157 DatePicker({ 158 start: new Date('1970-1-1'), 159 end: new Date('2100-1-1'), 160 selected: this.a 161 }) 162 } 163 } 164} 165 166@Entry 167@Component 168struct ViewB { 169 @State b: ClassB = new ClassB(new DateClass('2023-1-1')); 170 171 build() { 172 Column() { 173 ViewA({ label: 'date', a: this.b.a }) 174 175 Button(`parent update the new date`) 176 .onClick(() => { 177 this.b.a = new DateClass('2023-07-07'); 178 }) 179 Button(`ViewB: this.b = new ClassB(new DateClass('2023-08-20'))`) 180 .onClick(() => { 181 this.b = new ClassB(new DateClass('2023-08-20')); 182 }) 183 } 184 } 185} 186``` 187 188For a class that extends **Map**, the value changes of the **Map** instance can be observed. In addition, you can call the following APIs to update the instance: **set**, **clear**, and **delete**. For details, see [Extended Map Class](#extended-map-class). 189 190For a class that extends **Set**, the value changes of the **Set** instance can be observed. In addition, you can call the following APIs to update the instance: **add**, **clear**, and **delete**. For details, see [Extended Set Class](#extended-set-class). 191 192 193### Framework Behavior 194 1951. Initial rendering: 196 1. \@Observed causes all instances of the decorated class to be wrapped with an opaque proxy object, which takes over the setter and getter methods of the attributes of the class. 197 2. The \@ObjectLink decorated variable in the child component is initialized from the parent component and accepts the instance of the \@Observed decorated class. The \@ObjectLink decorated wrapped object registers itself with the \@Observed decorated class. 198 1992. Attribute update: When the attribute of the \@Observed decorated class is updated, the system uses the setter and getter of the proxy, traverses the \@ObjectLink decorated wrapped objects that depend on it, and notifies the data update. 200 201 202## Use Scenarios 203 204 205### Nested Object 206 207> **NOTE** 208> 209> **NextID** is used to generate a unique, persistent key for each array item during [ForEach rendering](./arkts-rendering-control-foreach.md), so as to identify the corresponding component. 210 211 212```ts 213// objectLinkNestedObjects.ets 214let NextID: number = 1; 215 216@Observed 217class Bag { 218 public id: number; 219 public size: number; 220 221 constructor(size: number) { 222 this.id = NextID++; 223 this.size = size; 224 } 225} 226 227@Observed 228class User { 229 public bag: Bag; 230 231 constructor(bag: Bag) { 232 this.bag = bag; 233 } 234} 235 236@Observed 237class Book { 238 public bookName: BookName; 239 240 constructor(bookName: BookName) { 241 this.bookName = bookName; 242 } 243} 244 245@Observed 246class BookName extends Bag { 247 public nameSize: number; 248 249 constructor(nameSize: number) { 250 // Invoke the parent class method to process nameSize. 251 super(nameSize); 252 this.nameSize = nameSize; 253 } 254} 255 256@Component 257struct ViewA { 258 label: string = 'ViewA'; 259 @ObjectLink bag: Bag; 260 261 build() { 262 Column() { 263 Text(`ViewA [${this.label}] this.bag.size = ${this.bag.size}`) 264 .fontColor('#ffffffff') 265 .backgroundColor('#ff3d9dba') 266 .width(320) 267 .height(50) 268 .borderRadius(25) 269 .margin(10) 270 .textAlign(TextAlign.Center) 271 Button(`ViewA: this.bag.size add 1`) 272 .width(320) 273 .backgroundColor('#ff17a98d') 274 .margin(10) 275 .onClick(() => { 276 this.bag.size += 1; 277 }) 278 } 279 } 280} 281 282@Component 283struct ViewC { 284 label: string = 'ViewC1'; 285 @ObjectLink bookName: BookName; 286 287 build() { 288 Row() { 289 Column() { 290 Text(`ViewC [${this.label}] this.bookName.size = ${this.bookName.size}`) 291 .fontColor('#ffffffff') 292 .backgroundColor('#ff3d9dba') 293 .width(320) 294 .height(50) 295 .borderRadius(25) 296 .margin(10) 297 .textAlign(TextAlign.Center) 298 Button(`ViewC: this.bookName.size add 1`) 299 .width(320) 300 .backgroundColor('#ff17a98d') 301 .margin(10) 302 .onClick(() => { 303 this.bookName.size += 1; 304 console.log('this.bookName.size:' + this.bookName.size) 305 }) 306 } 307 .width(320) 308 } 309 } 310} 311 312@Entry 313@Component 314struct ViewB { 315 @State user: User = new User(new Bag(0)); 316 @State child: Book = new Book(new BookName(0)); 317 318 build() { 319 Column() { 320 ViewA({ label: 'ViewA #1', bag: this.user.bag }) 321 .width(320) 322 ViewC({ label: 'ViewC #3', bookName: this.child.bookName }) 323 .width(320) 324 Button(`ViewB: this.child.bookName.size add 10`) 325 .width(320) 326 .backgroundColor('#ff17a98d') 327 .margin(10) 328 .onClick(() => { 329 this.child.bookName.size += 10 330 console.log('this.child.bookName.size:' + this.child.bookName.size) 331 }) 332 Button(`ViewB: this.user.bag = new Bag(10)`) 333 .width(320) 334 .backgroundColor('#ff17a98d') 335 .margin(10) 336 .onClick(() => { 337 this.user.bag = new Bag(10); 338 }) 339 Button(`ViewB: this.user = new User(new Bag(20))`) 340 .width(320) 341 .backgroundColor('#ff17a98d') 342 .margin(10) 343 .onClick(() => { 344 this.user = new User(new Bag(20)); 345 }) 346 } 347 } 348} 349``` 350 351 352 353The @Observed decorated **BookName** class can observe changes in the attributes inherited from the base class. 354 355 356Event handles in **ViewB**: 357 358 359- **this.user.bag = new Bag(10)** and **this.user = new User(new Bag(20))**: Change to the \@State decorated variable **size** and its attributes. 360 361- this.child.bookName.size += ... : Change at the second layer. Though \@State cannot observe changes at the second layer, the change of an attribute of \@Observed decorated **Bag**, which is attribute **size** in this example, can be observed by \@ObjectLink. 362 363 364Event handle in **ViewC**: 365 366 367- **this.bookName.size += 1**: A change to the \@ObjectLink decorated variable **size** causes the button label to be updated. Unlike \@Prop, \@ObjectLink does not have a copy of its source. Instead, \@ObjectLink creates a reference to its source. 368 369- The \@ObjectLink decorated variable is read-only. Assigning **this.bookName = new bookName(...)** is not allowed. Once value assignment occurs, the reference to the data source is reset and the synchronization is interrupted. 370 371 372### Object Array 373 374An object array is a frequently used data structure. The following example shows the usage of array objects. 375 376 377```ts 378let NextID: number = 1; 379 380@Observed 381class ClassA { 382 public id: number; 383 public c: number; 384 385 constructor(c: number) { 386 this.id = NextID++; 387 this.c = c; 388 } 389} 390 391@Component 392struct ViewA { 393 // The type of @ObjectLink of the child component ViewA is ClassA. 394 @ObjectLink a: ClassA; 395 label: string = 'ViewA1'; 396 397 build() { 398 Row() { 399 Button(`ViewA [${this.label}] this.a.c = ${this.a ? this.a.c : "undefined"}`) 400 .width(320) 401 .margin(10) 402 .onClick(() => { 403 this.a.c += 1; 404 }) 405 } 406 } 407} 408 409@Entry 410@Component 411struct ViewB { 412 // ViewB has the @State decorated ClassA[]. 413 @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]; 414 415 build() { 416 Column() { 417 ForEach(this.arrA, 418 (item: ClassA) => { 419 ViewA({ label: `#${item.id}`, a: item }) 420 }, 421 (item: ClassA): string => item.id.toString() 422 ) 423 // Initialize the @ObjectLink decorated variable using the array item in the @State decorated array, which is an instance of ClassA decorated by @Observed. 424 ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) 425 ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) 426 427 Button(`ViewB: reset array`) 428 .width(320) 429 .margin(10) 430 .onClick(() => { 431 this.arrA = [new ClassA(0), new ClassA(0)]; 432 }) 433 Button(`ViewB: push`) 434 .width(320) 435 .margin(10) 436 .onClick(() => { 437 this.arrA.push(new ClassA(0)) 438 }) 439 Button(`ViewB: shift`) 440 .width(320) 441 .margin(10) 442 .onClick(() => { 443 if (this.arrA.length > 0) { 444 this.arrA.shift() 445 } else { 446 console.log("length <= 0") 447 } 448 }) 449 Button(`ViewB: chg item property in middle`) 450 .width(320) 451 .margin(10) 452 .onClick(() => { 453 this.arrA[Math.floor(this.arrA.length / 2)].c = 10; 454 }) 455 Button(`ViewB: chg item property in middle`) 456 .width(320) 457 .margin(10) 458 .onClick(() => { 459 this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); 460 }) 461 } 462 } 463} 464``` 465 466 467 468- this.arrA[Math.floor(this.arrA.length/2)] = new ClassA(..): The change of this state variable triggers two updates. 469 1. ForEach: The value assignment of the array item causes the change of [itemGenerator](../reference/apis-arkui/arkui-ts/ts-rendering-control-foreach.md) of **ForEach**. Therefore, the array item is identified as changed, and the item builder of** ForEach** is executed to create a **ViewA** component instance. 470 2. ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }): The preceding update changes the second element in the array. Therefore, the **ViewA** component instance bound to **this.arrA[1]** is updated. 471 472- this.arrA.push(new ClassA(0)): The change of this state variable triggers two updates with different effects. 473 1. ForEach: The newly added **ClassA** object is unknown to the **ForEach** [itemGenerator](../reference/apis-arkui/arkui-ts/ts-rendering-control-foreach.md). The item builder of **ForEach** will be executed to create a **ViewA** component instance. 474 2. ViewA({ label: ViewA this.arrA[last], a: this.arrA[this.arrA.length-1] }): The last item of the array is changed. As a result, the second **View A** component instance is changed. For **ViewA({ label: ViewA this.arrA[first], a: this.arrA[0] })**, a change to the array does not trigger a change to the array item, so the first **View A** component instance is not refreshed. 475 476- this.arrA[Math.floor(this.arrA.length/2)].c: @State cannot observe changes at the second layer. However, as **ClassA** is decorated by \@Observed, the change of its attributes will be observed by \@ObjectLink. 477 478 479### Two-Dimensional Array 480 481@Observed class decoration is required for a two-dimensional array. You can declare an \@Observed decorated class that extends from **Array**. 482 483 484```ts 485@Observed 486class StringArray extends Array<String> { 487} 488``` 489 490Use **new StringArray()** to create an instance of **StringArray**. The **new** operator makes \@Observed take effect, which can observe the attribute changes of **StringArray**. 491 492Declare a class that extends from **Array**: **class StringArray extends Array\<String> {}** and create an instance of **StringArray**. The use of the **new** operator is required for the \@Observed class decorator to work properly. 493 494 495```ts 496@Observed 497class StringArray extends Array<String> { 498} 499 500@Component 501struct ItemPage { 502 @ObjectLink itemArr: StringArray; 503 504 build() { 505 Row() { 506 Text('ItemPage') 507 .width(100).height(100) 508 509 ForEach(this.itemArr, 510 (item: string | Resource) => { 511 Text(item) 512 .width(100).height(100) 513 }, 514 (item: string) => item 515 ) 516 } 517 } 518} 519 520@Entry 521@Component 522struct IndexPage { 523 @State arr: Array<StringArray> = [new StringArray(), new StringArray(), new StringArray()]; 524 525 build() { 526 Column() { 527 ItemPage({ itemArr: this.arr[0] }) 528 ItemPage({ itemArr: this.arr[1] }) 529 ItemPage({ itemArr: this.arr[2] }) 530 Divider() 531 532 533 ForEach(this.arr, 534 (itemArr: StringArray) => { 535 ItemPage({ itemArr: itemArr }) 536 }, 537 (itemArr: string) => itemArr[0] 538 ) 539 540 Divider() 541 542 Button('update') 543 .onClick(() => { 544 console.error('Update all items in arr'); 545 if ((this.arr[0] as Array<String>)[0] !== undefined) { 546 // We should have a real ID to use with ForEach, but we do not. 547 // Therefore, we need to make sure the pushed strings are unique. 548 this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`); 549 this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`); 550 this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`); 551 } else { 552 this.arr[0].push('Hello'); 553 this.arr[1].push('World'); 554 this.arr[2].push('!'); 555 } 556 }) 557 } 558 } 559} 560``` 561 562 563 564### Extended Map Class 565 566> **NOTE** 567> 568> Since API version 11, \@ObjectLink supports @Observed decorated classes extending from **Map** and the Map type. 569 570In the following example, the **myMap** variable is of the MyMap\<number, string\> type. When the button is clicked, the value of **myMap** changes, and the UI is re-rendered. 571 572```ts 573@Observed 574class ClassA { 575 public a: MyMap<number, string>; 576 577 constructor(a: MyMap<number, string>) { 578 this.a = a; 579 } 580} 581 582 583@Observed 584export class MyMap<K, V> extends Map<K, V> { 585 public name: string; 586 587 constructor(name?: string, args?: [K, V][]) { 588 super(args); 589 this.name = name ? name : "My Map"; 590 } 591 592 getName() { 593 return this.name; 594 } 595} 596 597@Entry 598@Component 599struct MapSampleNested { 600 @State message: ClassA = new ClassA(new MyMap("myMap", [[0, "a"], [1, "b"], [3, "c"]])); 601 602 build() { 603 Row() { 604 Column() { 605 MapSampleNestedChild({ myMap: this.message.a }) 606 } 607 .width('100%') 608 } 609 .height('100%') 610 } 611} 612 613@Component 614struct MapSampleNestedChild { 615 @ObjectLink myMap: MyMap<number, string> 616 617 build() { 618 Row() { 619 Column() { 620 ForEach(Array.from(this.myMap.entries()), (item: [number, string]) => { 621 Text(`${item[0]}`).fontSize(30) 622 Text(`${item[1]}`).fontSize(30) 623 Divider().strokeWidth(5) 624 }) 625 626 Button('set new one') 627 .width(200) 628 .margin(10) 629 .onClick(() => { 630 this.myMap.set(4, "d") 631 }) 632 Button('clear') 633 .width(200) 634 .margin(10) 635 .onClick(() => { 636 this.myMap.clear() 637 }) 638 Button('replace the first one') 639 .width(200) 640 .margin(10) 641 .onClick(() => { 642 this.myMap.set(0, "aa") 643 }) 644 Button('delete the first one') 645 .width(200) 646 .margin(10) 647 .onClick(() => { 648 this.myMap.delete(0) 649 }) 650 } 651 .width('100%') 652 } 653 .height('100%') 654 } 655} 656``` 657 658 659 660### Extended Set Class 661 662> **NOTE** 663> 664> Since API version 11, \@ObjectLink supports @Observed decorated classes extending from **Set** and the Set type. 665 666In the following example, the **mySet** variable is of the MySet\<number\> type. When the button is clicked, the value of **mySet** changes, and the UI is re-rendered. 667 668```ts 669@Observed 670class ClassA { 671 public a: MySet<number>; 672 673 constructor(a: MySet<number>) { 674 this.a = a; 675 } 676} 677 678 679@Observed 680export class MySet<T> extends Set<T> { 681 public name: string; 682 683 constructor(name?: string, args?: T[]) { 684 super(args); 685 this.name = name ? name : "My Set"; 686 } 687 688 getName() { 689 return this.name; 690 } 691} 692 693@Entry 694@Component 695struct SetSampleNested { 696 @State message: ClassA = new ClassA(new MySet("Set", [0, 1, 2, 3, 4])); 697 698 build() { 699 Row() { 700 Column() { 701 SetSampleNestedChild({ mySet: this.message.a }) 702 } 703 .width('100%') 704 } 705 .height('100%') 706 } 707} 708 709@Component 710struct SetSampleNestedChild { 711 @ObjectLink mySet: MySet<number> 712 713 build() { 714 Row() { 715 Column() { 716 ForEach(Array.from(this.mySet.entries()), (item: [number, number]) => { 717 Text(`${item}`).fontSize(30) 718 Divider() 719 }) 720 Button('set new one') 721 .width(200) 722 .margin(10) 723 .onClick(() => { 724 this.mySet.add(5) 725 }) 726 Button('clear') 727 .width(200) 728 .margin(10) 729 .onClick(() => { 730 this.mySet.clear() 731 }) 732 Button('delete the first one') 733 .width(200) 734 .margin(10) 735 .onClick(() => { 736 this.mySet.delete(0) 737 }) 738 } 739 .width('100%') 740 } 741 .height('100%') 742 } 743} 744``` 745 746 747 748## Union Type @ObjectLink 749 750@ObjectLink supports union types of @Observed decorated classes and **undefined** or **null**. In the following example, the type of **count** is ClassA | ClassB | undefined. If the attribute or type of **count** is changed when the button in the parent component **Page2** is clicked, the change will be synchronized to the child component. 751 752```ts 753@Observed 754class ClassA { 755 public a: number; 756 757 constructor(a: number) { 758 this.a = a; 759 } 760} 761 762@Observed 763class ClassB { 764 public b: number; 765 766 constructor(b: number) { 767 this.b = b; 768 } 769} 770 771@Entry 772@Component 773struct Page2 { 774 @State count: ClassA | ClassB | undefined = new ClassA(10) 775 776 build() { 777 Column() { 778 Child({ count: this.count }) 779 780 Button('change count property') 781 .onClick(() => { 782 // Determine the count type and update the attribute. 783 if (this.count instanceof ClassA) { 784 this.count.a += 1 785 } else if (this.count instanceof ClassB) { 786 this.count.b += 1 787 } else { 788 console.info('count is undefined, cannot change property') 789 } 790 }) 791 792 Button('change count to ClassA') 793 .onClick(() => { 794 // Assign the value to an instance of ClassA. 795 this.count = new ClassA(100) 796 }) 797 798 Button('change count to ClassB') 799 .onClick(() => { 800 // Assign the value to an instance of ClassA. 801 this.count = new ClassB(100) 802 }) 803 804 Button('change count to undefined') 805 .onClick(() => { 806 // Assign the value undefined. 807 this.count = undefined 808 }) 809 }.width('100%') 810 } 811} 812 813@Component 814struct Child { 815 @ObjectLink count: ClassA | ClassB | undefined 816 817 build() { 818 Column() { 819 Text(`count is instanceof ${this.count instanceof ClassA ? 'ClassA' : this.count instanceof ClassB ? 'ClassB' : 'undefined'}`) 820 .fontSize(30) 821 822 Text(`count's property is ${this.count instanceof ClassA ? this.count.a : this.count?.b}`).fontSize(15) 823 824 }.width('100%') 825 } 826} 827``` 828 829 830 831## FAQs 832 833### Assigning Value to @ObjectLink Decorated Variable in Child Component 834 835It is not allowed to assign a value to an @ObjectLink decorated variable in the child component. 836 837[Incorrect Usage] 838 839```ts 840@Observed 841class ClassA { 842 public c: number = 0; 843 844 constructor(c: number) { 845 this.c = c; 846 } 847} 848 849@Component 850struct ObjectLinkChild { 851 @ObjectLink testNum: ClassA; 852 853 build() { 854 Text(`ObjectLinkChild testNum ${this.testNum.c}`) 855 .onClick(() => { 856 // The @ObjectLink decorated variable cannot be assigned a value here. 857 this.testNum = new ClassA(47); 858 }) 859 } 860} 861 862@Entry 863@Component 864struct Parent { 865 @State testNum: ClassA[] = [new ClassA(1)]; 866 867 build() { 868 Column() { 869 Text(`Parent testNum ${this.testNum[0].c}`) 870 .onClick(() => { 871 this.testNum[0].c += 1; 872 }) 873 874 ObjectLinkChild({ testNum: this.testNum[0] }) 875 } 876 } 877} 878``` 879 880In this example, an attempt is made to assign a value to the @ObjectLink decorated variable by clicking **ObjectLinkChild**. 881 882``` 883this.testNum = new ClassA(47); 884``` 885 886This is not allowed. For @ObjectLink that implements two-way data synchronization, assigning a value is equivalent to updating the array item or class attribute in the parent component, which is not supported in TypeScript/JavaScript and will result in a runtime error. 887 888[Correct Usage] 889 890```ts 891@Observed 892class ClassA { 893 public c: number = 0; 894 895 constructor(c: number) { 896 this.c = c; 897 } 898} 899 900@Component 901struct ObjectLinkChild { 902 @ObjectLink testNum: ClassA; 903 904 build() { 905 Text(`ObjectLinkChild testNum ${this.testNum.c}`) 906 .onClick(() => { 907 // You can assign values to the attributes of the ObjectLink decorated object. 908 this.testNum.c = 47; 909 }) 910 } 911} 912 913@Entry 914@Component 915struct Parent { 916 @State testNum: ClassA[] = [new ClassA(1)]; 917 918 build() { 919 Column() { 920 Text(`Parent testNum ${this.testNum[0].c}`) 921 .onClick(() => { 922 this.testNum[0].c += 1; 923 }) 924 925 ObjectLinkChild({ testNum: this.testNum[0] }) 926 } 927 } 928} 929``` 930 931### UI Not Updated on Attribute Changes in Simple Nested Objects 932 933If you find your application UI not updating after an attribute in a nested object is changed, you may want to check the decorators in use. 934 935Each decorator has its scope of observable changes, and only those observed changes can cause the UI to update. The \@Observed decorator can observe the attribute changes of nested objects, while other decorators can observe only the changes at the first layer. 936 937[Incorrect Usage] 938 939In the following example, some UI components are not updated. 940 941 942```ts 943class ClassA { 944 a: number; 945 946 constructor(a: number) { 947 this.a = a; 948 } 949 950 getA(): number { 951 return this.a; 952 } 953 954 setA(a: number): void { 955 this.a = a; 956 } 957} 958 959class ClassC { 960 c: number; 961 962 constructor(c: number) { 963 this.c = c; 964 } 965 966 getC(): number { 967 return this.c; 968 } 969 970 setC(c: number): void { 971 this.c = c; 972 } 973} 974 975class ClassB extends ClassA { 976 b: number = 47; 977 c: ClassC; 978 979 constructor(a: number, b: number, c: number) { 980 super(a); 981 this.b = b; 982 this.c = new ClassC(c); 983 } 984 985 getB(): number { 986 return this.b; 987 } 988 989 setB(b: number): void { 990 this.b = b; 991 } 992 993 getC(): number { 994 return this.c.getC(); 995 } 996 997 setC(c: number): void { 998 return this.c.setC(c); 999 } 1000} 1001 1002 1003@Entry 1004@Component 1005struct MyView { 1006 @State b: ClassB = new ClassB(10, 20, 30); 1007 1008 build() { 1009 Column({ space: 10 }) { 1010 Text(`a: ${this.b.a}`) 1011 Button("Change ClassA.a") 1012 .onClick(() => { 1013 this.b.a += 1; 1014 }) 1015 1016 Text(`b: ${this.b.b}`) 1017 Button("Change ClassB.b") 1018 .onClick(() => { 1019 this.b.b += 1; 1020 }) 1021 1022 Text(`c: ${this.b.c.c}`) 1023 Button("Change ClassB.ClassC.c") 1024 .onClick(() => { 1025 // The Text component is not updated when clicked. 1026 this.b.c.c += 1; 1027 }) 1028 } 1029 } 1030} 1031``` 1032 1033- The UI is not updated when the last **Text** component Text('c: ${this.b.c.c}') is clicked. This is because, **\@State b: ClassB** can observe only the changes of the **this.b** attribute, such as **this.b.a**, **this.b.b**, and **this.b.c**, but cannot observe the attributes nested in the attribute, that is, **this.b.c.c** (attribute **c** is an attribute of the **ClassC** object nested in **b**). 1034 1035- To observe the attributes of nested object **ClassC**, you need to make the following changes: 1036 - Construct a child component for separate rendering of the **ClassC** instance. Then, in this child component, you can use \@ObjectLink or \@Prop to decorate **c : ClassC**. In general cases, use \@ObjectLink, unless local changes to the **ClassC** object are required. 1037 - The nested **ClassC** object must be decorated by \@Observed. When a **ClassC** object is created in **ClassB** (**ClassB(10, 20, 30)** in this example), it is wrapped in the ES6 proxy. When the **ClassC** attribute changes (this.b.c.c += 1), the \@ObjectLink decorated variable is notified of the change. 1038 1039[Correct Usage] 1040 1041The following example uses \@Observed/\@ObjectLink to observe property changes for nested objects. 1042 1043 1044```ts 1045class ClassA { 1046 a: number; 1047 1048 constructor(a: number) { 1049 this.a = a; 1050 } 1051 1052 getA(): number { 1053 return this.a; 1054 } 1055 1056 setA(a: number): void { 1057 this.a = a; 1058 } 1059} 1060 1061@Observed 1062class ClassC { 1063 c: number; 1064 1065 constructor(c: number) { 1066 this.c = c; 1067 } 1068 1069 getC(): number { 1070 return this.c; 1071 } 1072 1073 setC(c: number): void { 1074 this.c = c; 1075 } 1076} 1077 1078class ClassB extends ClassA { 1079 b: number = 47; 1080 c: ClassC; 1081 1082 constructor(a: number, b: number, c: number) { 1083 super(a); 1084 this.b = b; 1085 this.c = new ClassC(c); 1086 } 1087 1088 getB(): number { 1089 return this.b; 1090 } 1091 1092 setB(b: number): void { 1093 this.b = b; 1094 } 1095 1096 getC(): number { 1097 return this.c.getC(); 1098 } 1099 1100 setC(c: number): void { 1101 return this.c.setC(c); 1102 } 1103} 1104 1105@Component 1106struct ViewClassC { 1107 @ObjectLink c: ClassC; 1108 1109 build() { 1110 Column({ space: 10 }) { 1111 Text(`c: ${this.c.getC()}`) 1112 Button("Change C") 1113 .onClick(() => { 1114 this.c.setC(this.c.getC() + 1); 1115 }) 1116 } 1117 } 1118} 1119 1120@Entry 1121@Component 1122struct MyView { 1123 @State b: ClassB = new ClassB(10, 20, 30); 1124 1125 build() { 1126 Column({ space: 10 }) { 1127 Text(`a: ${this.b.a}`) 1128 Button("Change ClassA.a") 1129 .onClick(() => { 1130 this.b.a += 1; 1131 }) 1132 1133 Text(`b: ${this.b.b}`) 1134 Button("Change ClassB.b") 1135 .onClick(() => { 1136 this.b.b += 1; 1137 }) 1138 1139 ViewClassC({ c: this.b.c }) // Equivalent to Text(`c: ${this.b.c.c}`) 1140 Button("Change ClassB.ClassC.c") 1141 .onClick(() => { 1142 this.b.c.c += 1; 1143 }) 1144 } 1145 } 1146} 1147``` 1148 1149### UI Not Updated on Attribute Changes in Complex Nested Objects 1150 1151[Incorrect Usage] 1152 1153The following example creates a child component with an \@ObjectLink decorated variable to render **ParentCounter** with nested attributes. Specifically, **SubCounter** nested in **ParentCounter** is decorated with \@Observed. 1154 1155 1156```ts 1157let nextId = 1; 1158@Observed 1159class SubCounter { 1160 counter: number; 1161 constructor(c: number) { 1162 this.counter = c; 1163 } 1164} 1165@Observed 1166class ParentCounter { 1167 id: number; 1168 counter: number; 1169 subCounter: SubCounter; 1170 incrCounter() { 1171 this.counter++; 1172 } 1173 incrSubCounter(c: number) { 1174 this.subCounter.counter += c; 1175 } 1176 setSubCounter(c: number): void { 1177 this.subCounter.counter = c; 1178 } 1179 constructor(c: number) { 1180 this.id = nextId++; 1181 this.counter = c; 1182 this.subCounter = new SubCounter(c); 1183 } 1184} 1185@Component 1186struct CounterComp { 1187 @ObjectLink value: ParentCounter; 1188 build() { 1189 Column({ space: 10 }) { 1190 Text(`${this.value.counter}`) 1191 .fontSize(25) 1192 .onClick(() => { 1193 this.value.incrCounter(); 1194 }) 1195 Text(`${this.value.subCounter.counter}`) 1196 .onClick(() => { 1197 this.value.incrSubCounter(1); 1198 }) 1199 Divider().height(2) 1200 } 1201 } 1202} 1203@Entry 1204@Component 1205struct ParentComp { 1206 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 1207 build() { 1208 Row() { 1209 Column() { 1210 CounterComp({ value: this.counter[0] }) 1211 CounterComp({ value: this.counter[1] }) 1212 CounterComp({ value: this.counter[2] }) 1213 Divider().height(5) 1214 ForEach(this.counter, 1215 (item: ParentCounter) => { 1216 CounterComp({ value: item }) 1217 }, 1218 (item: ParentCounter) => item.id.toString() 1219 ) 1220 Divider().height(5) 1221 // First click event 1222 Text('Parent: incr counter[0].counter') 1223 .fontSize(20).height(50) 1224 .onClick(() => { 1225 this.counter[0].incrCounter(); 1226 // The value increases by 10 each time the event is triggered. 1227 this.counter[0].incrSubCounter(10); 1228 }) 1229 // Second click event 1230 Text('Parent: set.counter to 10') 1231 .fontSize(20).height(50) 1232 .onClick(() => { 1233 // The value cannot be set to 10, and the UI is not updated. 1234 this.counter[0].setSubCounter(10); 1235 }) 1236 Text('Parent: reset entire counter') 1237 .fontSize(20).height(50) 1238 .onClick(() => { 1239 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 1240 }) 1241 } 1242 } 1243 } 1244} 1245``` 1246 1247For the **onClick** event of **Text('Parent: incr counter[0].counter')**, **this.counter[0].incrSubCounter(10)** calls the **incrSubCounter** method to increase the **counter** value of **SubCounter** by 10. The UI is updated to reflect the change. 1248 1249However, when **this.counter[0].setSubCounter(10)** is called in **onClick** of **Text('Parent: set.counter to 10')**, the **counter** value of **SubCounter** cannot be reset to **10**. 1250 1251**incrSubCounter** and **setSubCounter** are functions of the same **SubCounter**. The UI can be correctly updated when **incrSubCounter** is called for the first click event. However, the UI is not updated when **setSubCounter** is called for the second click event. Actually neither **incrSubCounter** nor **setSubCounter** can trigger an update of **Text('${this.value.subCounter.counter}')**. This is because **\@ObjectLink value: ParentCounter** can only observe the attributes of **ParentCounter**. **this.value.subCounter.counter** is an attribute of **SubCounter** and therefore cannot be observed. 1252 1253However, when **this.counter[0].incrCounter()** is called for the first click event, it marks **\@ObjectLink value: ParentCounter** in the **CounterComp** component as changed. In this case, an update of **Text('${this.value.subCounter.counter}')** is triggered. If **this.counter[0].incrCounter()** is deleted from the first click event, the UI cannot be updated. 1254 1255[Correct Usage] 1256 1257To solve the preceding problem, you can use the following method to directly observe the attributes in **SubCounter** so that the **this.counter[0].setSubCounter(10)** API works: 1258 1259 1260```ts 1261@ObjectLink value: ParentCounter = new ParentCounter(0); 1262@ObjectLink subValue: SubCounter = new SubCounter(0); 1263``` 1264 1265This approach enables \@ObjectLink to serve as a proxy for the attributes of the **ParentCounter** and **SubCounter** classes. In this way, the attribute changes of the two classes can be observed and trigger UI update. Even if **this.counter[0].incrCounter()** is deleted, the UI can be updated correctly. 1266 1267This approach can be used to implement "two-layer" observation, that is, observation of external objects and internal nested objects. However, it is only applicable to the \@ObjectLink decorator, but not to \@Prop (\@Prop passes objects through deep copy). For details, see [Differences Between \@Prop and \@ObjectLink](#differences-between-prop-and-objectlink). 1268 1269 1270```ts 1271let nextId = 1; 1272 1273@Observed 1274class SubCounter { 1275 counter: number; 1276 1277 constructor(c: number) { 1278 this.counter = c; 1279 } 1280} 1281 1282@Observed 1283class ParentCounter { 1284 id: number; 1285 counter: number; 1286 subCounter: SubCounter; 1287 1288 incrCounter() { 1289 this.counter++; 1290 } 1291 1292 incrSubCounter(c: number) { 1293 this.subCounter.counter += c; 1294 } 1295 1296 setSubCounter(c: number): void { 1297 this.subCounter.counter = c; 1298 } 1299 1300 constructor(c: number) { 1301 this.id = nextId++; 1302 this.counter = c; 1303 this.subCounter = new SubCounter(c); 1304 } 1305} 1306 1307@Component 1308struct CounterComp { 1309 @ObjectLink value: ParentCounter; 1310 1311 build() { 1312 Column({ space: 10 }) { 1313 Text(`${this.value.counter}`) 1314 .fontSize(25) 1315 .onClick(() => { 1316 this.value.incrCounter(); 1317 }) 1318 CounterChild({ subValue: this.value.subCounter }) 1319 Divider().height(2) 1320 } 1321 } 1322} 1323 1324@Component 1325struct CounterChild { 1326 @ObjectLink subValue: SubCounter; 1327 1328 build() { 1329 Text(`${this.subValue.counter}`) 1330 .onClick(() => { 1331 this.subValue.counter += 1; 1332 }) 1333 } 1334} 1335 1336@Entry 1337@Component 1338struct ParentComp { 1339 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 1340 1341 build() { 1342 Row() { 1343 Column() { 1344 CounterComp({ value: this.counter[0] }) 1345 CounterComp({ value: this.counter[1] }) 1346 CounterComp({ value: this.counter[2] }) 1347 Divider().height(5) 1348 ForEach(this.counter, 1349 (item: ParentCounter) => { 1350 CounterComp({ value: item }) 1351 }, 1352 (item: ParentCounter) => item.id.toString() 1353 ) 1354 Divider().height(5) 1355 Text('Parent: reset entire counter') 1356 .fontSize(20).height(50) 1357 .onClick(() => { 1358 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 1359 }) 1360 Text('Parent: incr counter[0].counter') 1361 .fontSize(20).height(50) 1362 .onClick(() => { 1363 this.counter[0].incrCounter(); 1364 this.counter[0].incrSubCounter(10); 1365 }) 1366 Text('Parent: set.counter to 10') 1367 .fontSize(20).height(50) 1368 .onClick(() => { 1369 this.counter[0].setSubCounter(10); 1370 }) 1371 } 1372 } 1373 } 1374} 1375``` 1376 1377### Differences Between \@Prop and \@ObjectLink 1378 1379In the following example, the \@ObjectLink decorated variable is a reference to the data source. That is, **this.value.subValue** and **this.subValue** are different references to the same object. Therefore, when the click handler of **CounterComp** is clicked, both **this.value.subCounter.counter** and **this.subValue.counter** change, and the corresponding component **Text (this.subValue.counter: ${this.subValue.counter})** is re-rendered. 1380 1381 1382```ts 1383let nextId = 1; 1384 1385@Observed 1386class SubCounter { 1387 counter: number; 1388 1389 constructor(c: number) { 1390 this.counter = c; 1391 } 1392} 1393 1394@Observed 1395class ParentCounter { 1396 id: number; 1397 counter: number; 1398 subCounter: SubCounter; 1399 1400 incrCounter() { 1401 this.counter++; 1402 } 1403 1404 incrSubCounter(c: number) { 1405 this.subCounter.counter += c; 1406 } 1407 1408 setSubCounter(c: number): void { 1409 this.subCounter.counter = c; 1410 } 1411 1412 constructor(c: number) { 1413 this.id = nextId++; 1414 this.counter = c; 1415 this.subCounter = new SubCounter(c); 1416 } 1417} 1418 1419@Component 1420struct CounterComp { 1421 @ObjectLink value: ParentCounter; 1422 1423 build() { 1424 Column({ space: 10 }) { 1425 CountChild({ subValue: this.value.subCounter }) 1426 Text(`this.value.counter: increase 7 `) 1427 .fontSize(30) 1428 .onClick(() => { 1429 // click handler, Text(`this.subValue.counter: ${this.subValue.counter}`) will update 1430 this.value.incrSubCounter(7); 1431 }) 1432 Divider().height(2) 1433 } 1434 } 1435} 1436 1437@Component 1438struct CountChild { 1439 @ObjectLink subValue: SubCounter; 1440 1441 build() { 1442 Text(`this.subValue.counter: ${this.subValue.counter}`) 1443 .fontSize(30) 1444 } 1445} 1446 1447@Entry 1448@Component 1449struct ParentComp { 1450 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 1451 1452 build() { 1453 Row() { 1454 Column() { 1455 CounterComp({ value: this.counter[0] }) 1456 CounterComp({ value: this.counter[1] }) 1457 CounterComp({ value: this.counter[2] }) 1458 Divider().height(5) 1459 ForEach(this.counter, 1460 (item: ParentCounter) => { 1461 CounterComp({ value: item }) 1462 }, 1463 (item: ParentCounter) => item.id.toString() 1464 ) 1465 Divider().height(5) 1466 Text('Parent: reset entire counter') 1467 .fontSize(20).height(50) 1468 .onClick(() => { 1469 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 1470 }) 1471 Text('Parent: incr counter[0].counter') 1472 .fontSize(20).height(50) 1473 .onClick(() => { 1474 this.counter[0].incrCounter(); 1475 this.counter[0].incrSubCounter(10); 1476 }) 1477 Text('Parent: set.counter to 10') 1478 .fontSize(20).height(50) 1479 .onClick(() => { 1480 this.counter[0].setSubCounter(10); 1481 }) 1482 } 1483 } 1484 } 1485} 1486``` 1487 1488The following figure shows how \@ObjectLink works. 1489 1490 1491 1492[Incorrect Usage] 1493 1494If \@Prop is used instead of \@ObjectLink, then: When the first click handler is clicked, the UI is updated properly; However, when the second **onClick** event occurs, the first **Text** component of **CounterComp** is not re-rendered, because \@Prop makes a local copy of the variable. 1495 1496 **this.value.subCounter** and **this.subValue** are not the same object. Therefore, the change of **this.value.subCounter** does not change the copy object of **this.subValue**, and **Text(this.subValue.counter: ${this.subValue.counter})** is not re-rendered. 1497 1498```ts 1499@Component 1500struct CounterComp { 1501 @Prop value: ParentCounter = new ParentCounter(0); 1502 @Prop subValue: SubCounter = new SubCounter(0); 1503 build() { 1504 Column({ space: 10 }) { 1505 Text(`this.subValue.counter: ${this.subValue.counter}`) 1506 .fontSize(20) 1507 .onClick(() => { 1508 // 1st click handler 1509 this.subValue.counter += 7; 1510 }) 1511 Text(`this.value.counter: increase 7 `) 1512 .fontSize(20) 1513 .onClick(() => { 1514 // 2nd click handler 1515 this.value.incrSubCounter(7); 1516 }) 1517 Divider().height(2) 1518 } 1519 } 1520} 1521``` 1522 1523The following figure shows how \@Prop works. 1524 1525 1526 1527[Correct Usage] 1528 1529Make only one copy of \@Prop value: ParentCounter from **ParentComp** to **CounterComp**. Do not make another copy of **SubCounter**. 1530 1531- Use only one **\@Prop counter: Counter** in the **CounterComp** component. 1532 1533- Add another child component **SubCounterComp** that contains **\@ObjectLink subCounter: SubCounter**. This \@ObjectLink ensures that changes to the **SubCounter** object attributes are observed and the UI is updated properly. 1534 1535- **\@ObjectLink subCounter: SubCounter** shares the same **SubCounter** object with **this.counter.subCounter** of **CounterComp**. 1536 1537 1538 1539```ts 1540let nextId = 1; 1541 1542@Observed 1543class SubCounter { 1544 counter: number; 1545 constructor(c: number) { 1546 this.counter = c; 1547 } 1548} 1549 1550@Observed 1551class ParentCounter { 1552 id: number; 1553 counter: number; 1554 subCounter: SubCounter; 1555 incrCounter() { 1556 this.counter++; 1557 } 1558 incrSubCounter(c: number) { 1559 this.subCounter.counter += c; 1560 } 1561 setSubCounter(c: number): void { 1562 this.subCounter.counter = c; 1563 } 1564 constructor(c: number) { 1565 this.id = nextId++; 1566 this.counter = c; 1567 this.subCounter = new SubCounter(c); 1568 } 1569} 1570 1571@Component 1572struct SubCounterComp { 1573 @ObjectLink subValue: SubCounter; 1574 build() { 1575 Text(`SubCounterComp: this.subValue.counter: ${this.subValue.counter}`) 1576 .onClick(() => { 1577 // 2nd click handler 1578 this.subValue.counter = 7; 1579 }) 1580 } 1581} 1582@Component 1583struct CounterComp { 1584 @Prop value: ParentCounter; 1585 build() { 1586 Column({ space: 10 }) { 1587 Text(`this.value.incrCounter(): this.value.counter: ${this.value.counter}`) 1588 .fontSize(20) 1589 .onClick(() => { 1590 // 1st click handler 1591 this.value.incrCounter(); 1592 }) 1593 SubCounterComp({ subValue: this.value.subCounter }) 1594 Text(`this.value.incrSubCounter()`) 1595 .onClick(() => { 1596 // 3rd click handler 1597 this.value.incrSubCounter(77); 1598 }) 1599 Divider().height(2) 1600 } 1601 } 1602} 1603@Entry 1604@Component 1605struct ParentComp { 1606 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 1607 build() { 1608 Row() { 1609 Column() { 1610 CounterComp({ value: this.counter[0] }) 1611 CounterComp({ value: this.counter[1] }) 1612 CounterComp({ value: this.counter[2] }) 1613 Divider().height(5) 1614 ForEach(this.counter, 1615 (item: ParentCounter) => { 1616 CounterComp({ value: item }) 1617 }, 1618 (item: ParentCounter) => item.id.toString() 1619 ) 1620 Divider().height(5) 1621 Text('Parent: reset entire counter') 1622 .fontSize(20).height(50) 1623 .onClick(() => { 1624 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 1625 }) 1626 Text('Parent: incr counter[0].counter') 1627 .fontSize(20).height(50) 1628 .onClick(() => { 1629 this.counter[0].incrCounter(); 1630 this.counter[0].incrSubCounter(10); 1631 }) 1632 Text('Parent: set.counter to 10') 1633 .fontSize(20).height(50) 1634 .onClick(() => { 1635 this.counter[0].setSubCounter(10); 1636 }) 1637 } 1638 } 1639 } 1640} 1641``` 1642 1643 1644The following figure shows the copy relationship. 1645 1646 1647 1648 1649### Member Variable Changes in the @Observed Decorated Class Constructor Not Taking Effect 1650 1651In state management, @Observed decorated classes are wrapped with a proxy. When a member variable of a class is changed in a component, the proxy intercepts the change. When the value in the data source is changed, the proxy notifies the bound component of the change. In this way, the change can be observed and trigger UI re-rendering. 1652 1653If the value change of a member variable occurs in the class constructor, the change does not pass through the proxy (because the change occurs in the data source). Therefore, even if the change is successful with a timer in the class constructor, the UI cannot be re-rendered. 1654 1655[Incorrect Usage] 1656 1657```ts 1658@Observed 1659class RenderClass { 1660 waitToRender: boolean = false; 1661 1662 constructor() { 1663 setTimeout(() => { 1664 this.waitToRender = true; 1665 console.log("Change waitToRender to" + this.waitToRender); 1666 }, 1000) 1667 } 1668} 1669 1670@Entry 1671@Component 1672struct Index { 1673 @State @Watch('renderClassChange') renderClass: RenderClass = new RenderClass(); 1674 @State textColor: Color = Color.Black; 1675 1676 renderClassChange() { 1677 console.log("Render Class Change waitToRender is" + this.renderClass.waitToRender); 1678 } 1679 1680 build() { 1681 Row() { 1682 Column() { 1683 Text("Render Class waitToRender is" + this.renderClass.waitToRender) 1684 .fontSize(20) 1685 .fontColor(this.textColor) 1686 Button("Show") 1687 .onClick(() => { 1688 // It is not recommended to use other state variables to forcibly re-render the UI. This example is used to check whether the value of waitToRender is updated. 1689 this.textColor = Color.Red; 1690 }) 1691 } 1692 .width('100%') 1693 } 1694 .height('100%') 1695 } 1696} 1697``` 1698 1699In the preceding example, a timer is used in the constructor of **RenderClass**. Though the value of **waitToRender** changes 1 second later, the UI is not re-rendered. After the button is clicked to forcibly refresh the **Text** component, you can see that the value of **waitToRender** is changed to **true**. 1700 1701[Correct Usage] 1702 1703```ts 1704@Observed 1705class RenderClass { 1706 waitToRender: boolean = false; 1707 1708 constructor() { 1709 } 1710} 1711 1712@Entry 1713@Component 1714struct Index { 1715 @State @Watch('renderClassChange') renderClass: RenderClass = new RenderClass(); 1716 1717 renderClassChange() { 1718 console.log("Render Class Change waitToRender is" + this.renderClass.waitToRender); 1719 } 1720 1721 onPageShow() { 1722 setTimeout(() => { 1723 this.renderClass.waitToRender = true; 1724 console.log("Change renderClass to: " + this.renderClass.waitToRender); 1725 }, 1000) 1726 } 1727 1728 build() { 1729 Row() { 1730 Column() { 1731 Text ("Render Class Wait To Render is"+ this.renderClass.waitToRender) 1732 .fontSize(20) 1733 } 1734 .width('100%') 1735 } 1736 .height('100%') 1737 } 1738} 1739``` 1740 1741In the preceding example, the timer is moved to the component. In this case, the page displays "Render Class Wait To Render is false". When the timer is triggered, the value of renderClass is changed, triggering the [@Watch](./arkts-watch.md) callback. As a result, page content changes from "Render Class Change waitToRender is false" to "Render Class Change waitToRender is true". 1742 1743In sum, it is recommended that you change the class members decorated by @Observed in components to implement UI re-rendering. 1744 1745### ObjectLink Re-render Depends on the Custom Component 1746 1747```ts 1748@Observed 1749class Person { 1750 name: string = ''; 1751 age: number = 0; 1752 1753 constructor(name: string, age: number) { 1754 this.name = name; 1755 this.age = age; 1756 } 1757} 1758 1759@Observed 1760class Persons { 1761 person: Person; 1762 1763 constructor(person: Person) { 1764 this.person = person; 1765 } 1766} 1767 1768@Entry 1769@Component 1770struct Parent { 1771 @State pers01: Persons = new Persons(new Person('1', 1)); 1772 1773 build() { 1774 Column() { 1775 Child01({ pers: this.pers01 }); 1776 } 1777 } 1778} 1779 1780@Component 1781struct Child01 { 1782 @ObjectLink @Watch('onChange01') pers: Persons; 1783 1784 onChange01() { 1785 console.log(':::onChange01:' + this.pers.person.name); // 2 1786 } 1787 1788 build() { 1789 Column() { 1790 Text(this.pers.person.name).height(40) 1791 Child02({ 1792 per: this.pers.person, selectItemBlock: () => { 1793 console.log(':::selectItemBlock before', this.pers.person.name); // 1 1794 this.pers.person = new Person('2', 2); 1795 console.log(':::selectItemBlock after', this.pers.person.name); // 3 1796 } 1797 }) 1798 } 1799 } 1800} 1801 1802@Component 1803struct Child02 { 1804 @ObjectLink @Watch('onChange02') per: Person; 1805 selectItemBlock?: () => void; 1806 1807 onChange02() { 1808 console.log(':::onChange02:' + this.per.name); // 5 1809 } 1810 1811 build() { 1812 Column() { 1813 Button(this.per.name) 1814 .height(40) 1815 .onClick(() => { 1816 this.onClickFType(); 1817 }) 1818 } 1819 } 1820 1821 private onClickFType() { 1822 if (this.selectItemBlock) { 1823 this.selectItemBlock(); 1824 } 1825 console.log(':::-------- this.per.name in Child02 is still:' + this.per.name); // 4 1826 } 1827} 1828``` 1829 1830The data source notifies that the @ObjectLink re-render depends on the re-render function of the custom component to which @ObjectLink belongs, which uses an asynchronous callback. In the preceding example, **Parent** contains **Child01**, and the latter contains **Child02**. When click the button, the **Child01** points to the **Child02**. In this case, the click event of **Child02** is called. The log printing order is **1** > **2** > **3** > **4** > **5**. When log 4 is printed, the click event ends, in this case, only the child component **Child02** is marked as dirty. The update of **Child02** needs to wait for the next VSYCN. And the @ObjectLink re-render depends on the re-render function of the custom component to which @ObjectLink belongs. Therefore, the value of **this.per.name** in log 4 is still **1**. 1831 1832When the **@ObjectLink @Watch('onChange02') per: Person** is executed, the re-render function of **Child02** has been executed, and @ObjectLink has been notified to be re-rendered. Therefore, the value of log 5 is **2**. 1833 1834The meaning of the log is as follows: 1835- Log 1: Before assigning a value to **Child01 @ObjectLink @Watch('onChange01') pers: Persons**. 1836 1837- Log 2: Assign a value to **Child01 @ObjectLink @Watch('onChange01') pers: Persons** and execute its @Watch function synchronously. 1838 1839- Log 3: After assigning a value to **Child01 @ObjectLink @Watch('onChange01') pers: Persons**. 1840 1841- Log 4: After **selectItemBlock** in the **onClickFType** method is executed, the value is marked as dirty and the latest value of **Child02 @ObjectLink @Watch('onChange02') per: Person** is not re-rendered. Therefore, the value of **this.per.name** in log 4 is still **1**. 1842 1843- Log 5: The next VSYNC triggers **Child02** re-rendering. **@ObjectLink @Watch('onChange02') per: Person** is re-rendered and its @Watch method is triggered. In this case, the new value of the **@ObjectLink @Watch('onChange02') per: Person** is **2**. 1844 1845The parent-child synchronization principle of @Prop is the same as that of @ObjectLink. 1846 1847When **this.pers.person.name** is changed in **selectItemBlock**, this change takes effect immediately. In this case, the value of log 4 is **2**. 1848 1849```ts 1850Child02({ 1851 per: this.pers.person, selectItemBlock: () => { 1852 console.log(':::selectItemBlock before', this.pers.person.name); // 1 1853 this.pers.person.name = 2; 1854 console.log(':::selectItemBlock after', this.pers.person.name); // 3 1855 } 1856}) 1857``` 1858 1859The Text component in **Child01** is not re-rendered because **this.pers.person.name** is a value with two-layer nesting. 1860 1861<!--no_check-->