1# \@ObservedV2 and \@Trace Decorators: Observing Class Property Changes 2 3To allow the state management framework to observe properties in class objects, you can use the \@ObservedV2 decorator and \@Trace decorator to decorate classes and properties in classes. 4 5>**NOTE** 6> 7>The \@ObservedV2 and \@Trace decorators are supported since API version 12. 8> 9 10## Overview 11 12The \@ObservedV2 and \@Trace decorators are used to decorate classes and properties in classes so that changes to the classes and properties can be observed. 13 14- \@ObservedV2 and \@Trace must come in pairs. Using either of them alone does not work. 15- If a property decorated by \@Trace changes, only the component bound to the property is instructed to re-render. 16- In a nested class, changes to its property trigger UI re-renders only when the property is decorated by \@Trace and the class is decorated by \@ObservedV2. 17- In an inherited class, changes to a property of the parent or child class trigger UI re-renders only when the property is decorated by \@Trace and the owning class is decorated by \@ObservedV2. 18- Attributes that are not decorated by \@Trace cannot detect changes nor trigger UI re-renders. 19- Instances of \@ObservedV2 decorated classes cannot be serialized using **JSON.stringify**. 20 21## Limitations of State Management V1 on Observability of Properties in Nested Class Objects 22 23With state management V1, properties of nested class objects are not directly observable. 24 25```ts 26@Observed 27class Father { 28 son: Son; 29 30 constructor(name: string, age: number) { 31 this.son = new Son(name, age); 32 } 33} 34@Observed 35class Son { 36 name: string; 37 age: number; 38 39 constructor(name: string, age: number) { 40 this.name = name; 41 this.age = age; 42 } 43} 44@Entry 45@Component 46struct Index { 47 @State father: Father = new Father("John", 8); 48 49 build() { 50 Row() { 51 Column() { 52 Text(`name: ${this.father.son.name} age: ${this.father.son.age}`) 53 .fontSize(50) 54 .fontWeight(FontWeight.Bold) 55 .onClick(() => { 56 this.father.son.age++; 57 }) 58 } 59 .width('100%') 60 } 61 .height('100%') 62 } 63} 64``` 65 66In the preceding example, clicking the **Text** component increases the value of **age**, but does not trigger UI re-renders. The reason is that, the **age** property is in a nested class and not observable to the current state management framework. To resolve this issue, state management V1 uses [\@ObjectLink](arkts-observed-and-objectlink.md) with custom components. 67 68```ts 69@Observed 70class Father { 71 son: Son; 72 73 constructor(name: string, age: number) { 74 this.son = new Son(name, age); 75 } 76} 77@Observed 78class Son { 79 name: string; 80 age: number; 81 82 constructor(name: string, age: number) { 83 this.name = name; 84 this.age = age; 85 } 86} 87@Component 88struct Child { 89 @ObjectLink son: Son; 90 91 build() { 92 Row() { 93 Column() { 94 Text(`name: ${this.son.name} age: ${this.son.age}`) 95 .fontSize(50) 96 .fontWeight(FontWeight.Bold) 97 .onClick(() => { 98 this.son.age++; 99 }) 100 } 101 .width('100%') 102 } 103 .height('100%') 104 } 105} 106@Entry 107@Component 108struct Index { 109 @State father: Father = new Father("John", 8); 110 111 build() { 112 Column() { 113 Child({son: this.father.son}) 114 } 115 } 116} 117``` 118 119Yet, this approach has its drawbacks: If the nesting level is deep, the code becomes unnecessarily complicated and the usability poor. This is where the class decorator \@ObservedV2 and member property decorator \@Trace come into the picture. 120 121## Decorator Description 122 123| \@ObservedV2 Decorator| Description | 124| ------------------ | ----------------------------------------------------- | 125| Decorator parameters | None. | 126| Class decorator | Decorates a class. You must use **new** to create a class object before defining the class.|| 127 128| \@Trace member property decorator| Description | 129| --------------------- | ------------------------------------------------------------ | 130| Decorator parameters | None. | 131| Allowed variable types | Member properties in classes in any of the following types: number, string, boolean, class, Array, Date, Map, Set| 132 133## Observed Changes 134 135In classes decorated by \@ObservedV2, properties decorated by \@Trace are observable. This means that, any of the following changes can be observed and will trigger UI re-renders of bound components: 136 137- Changes to properties decorated by \@Trace in nested classes decorated by \@ObservedV2 138 139```ts 140@ObservedV2 141class Son { 142 @Trace age: number = 100; 143} 144class Father { 145 son: Son = new Son(); 146} 147@Entry 148@ComponentV2 149struct Index { 150 father: Father = new Father(); 151 152 build() { 153 Column() { 154 // If age is changed, the Text component is re-rendered. 155 Text(`${this.father.son.age}`) 156 .onClick(() => { 157 this.father.son.age++; 158 }) 159 } 160 } 161} 162 163``` 164 165- Changes to properties decorated by \@Trace in inherited classes decorated by \@ObservedV2 166 167```ts 168@ObservedV2 169class Father { 170 @Trace name: string = "Tom"; 171} 172class Son extends Father { 173} 174@Entry 175@ComponentV2 176struct Index { 177 son: Son = new Son(); 178 179 build() { 180 Column() { 181 // If name is changed, the Text component is re-rendered. 182 Text(`${this.son.name}`) 183 .onClick(() => { 184 this.son.name = "Jack"; 185 }) 186 } 187 } 188} 189``` 190 191- Changes to static properties decorated by \@Trace in classes decorated by \@ObservedV2 192 193```ts 194@ObservedV2 195class Manager { 196 @Trace static count: number = 1; 197} 198@Entry 199@ComponentV2 200struct Index { 201 build() { 202 Column() { 203 // If count is changed, the Text component is re-rendered. 204 Text(`${Manager.count}`) 205 .onClick(() => { 206 Manager.count++; 207 }) 208 } 209 } 210} 211``` 212 213- Changes caused by the APIs listed below to properties of built-in types decorated by \@Trace 214 215 | Type | APIs that can observe changes | 216 | ----- | ------------------------------------------------------------ | 217 | Array | push, pop, shift, unshift, splice, copyWithin, fill, reverse, sort| 218 | Date | setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds | 219 | Map | set, clear, delete | 220 | Set | add, clear, delete | 221 222## Constraints 223 224Note the following constraints when using the \@ObservedV2 and \@Trace decorators: 225 226- The member property that is not decorated by \@Trace cannot trigger UI re-renders. 227 228```ts 229@ObservedV2 230class Person { 231 id: number = 0; 232 @Trace age: number = 8; 233} 234@Entry 235@ComponentV2 236struct Index { 237 person: Person = new Person(); 238 239 build() { 240 Column() { 241 // age is decorated by @Trace and can trigger re-renders when used in the UI. 242 Text(`${this.person.age}`) 243 .onClick(() => { 244 this.person.age++; // The click event can trigger a UI re-render. 245 }) 246 // id is not decorated by @Trace and cannot trigger re-renders when used in the UI. 247 Text(`${this.person.id}`) // UI is not re-rendered when id changes. 248 .onClick(() => { 249 this.person.id++; // The click event cannot trigger a UI re-render. 250 }) 251 } 252 } 253} 254``` 255 256- \@ObservedV2 can decorate only classes. 257 258```ts 259@ObservedV2 // Incorrect usage. An error is reported during compilation. 260struct Index { 261 build() { 262 } 263} 264``` 265 266- \@Trace cannot be used in classes that are not decorated by \@ObservedV2. 267 268```ts 269class User { 270 id: number = 0; 271 @Trace name: string = "Tom"; // Incorrect usage. An error is reported at compile time. 272} 273``` 274 275- \@Trace is a decorator for properties in classes and cannot be used in a struct. 276 277```ts 278@ComponentV2 279struct Comp { 280 @Trace message: string = "Hello World"; // Incorrect usage. An error is reported at compile time. 281 282 build() { 283 } 284} 285``` 286 287- \@ObservedV2 and \@Trace cannot be used together with [\@Observed](arkts-observed-and-objectlink.md) and [\@Track](arkts-track.md). 288 289```ts 290@Observed 291class User { 292 @Trace name: string = "Tom"; // Incorrect usage. An error is reported at compile time. 293} 294 295@ObservedV2 296class Person { 297 @Track name: string = "Jack"; // Incorrect usage. An error is reported at compile time. 298} 299``` 300 301- Classes decorated by @ObservedV2 and @Trace cannot be used together with [\@State](arkts-state.md) or other decorators of V1. Otherwise, an error is reported at compile time. 302 303```ts 304// @State is used as an example. 305@ObservedV2 306class Job { 307 @Trace jobName: string = "Teacher"; 308} 309@ObservedV2 310class Info { 311 @Trace name: string = "Tom"; 312 @Trace age: number = 25; 313 job: Job = new Job(); 314} 315@Entry 316@Component 317struct Index { 318 @State info: Info = new Info(); // As @State is not allowed here, an error is reported at compile time. 319 320 build() { 321 Column() { 322 Text(`name: ${this.info.name}`) 323 Text(`age: ${this.info.age}`) 324 Text(`jobName: ${this.info.job.jobName}`) 325 Button("change age") 326 .onClick(() => { 327 this.info.age++; 328 }) 329 Button("Change job") 330 .onClick(() => { 331 this.info.job.jobName = "Doctor"; 332 }) 333 } 334 } 335} 336``` 337 338- Classes extended from \@ObservedV2 cannot be used together with [\@State](arkts-state.md) or other decorators of V1. Otherwise, an error is reported during running. 339 340```ts 341// @State is used as an example. 342@ObservedV2 343class Job { 344 @Trace jobName: string = "Teacher"; 345} 346@ObservedV2 347class Info { 348 @Trace name: string = "Tom"; 349 @Trace age: number = 25; 350 job: Job = new Job(); 351} 352class Message extends Info { 353 constructor() { 354 super(); 355 } 356} 357@Entry 358@Component 359struct Index { 360 @State message: Message = new Message(); // As @State is not allowed here, an error is reported during running. 361 362 build() { 363 Column() { 364 Text(`name: ${this.message.name}`) 365 Text(`age: ${this.message.age}`) 366 Text(`jobName: ${this.message.job.jobName}`) 367 Button("change age") 368 .onClick(() => { 369 this.message.age++; 370 }) 371 Button("Change job") 372 .onClick(() => { 373 this.message.job.jobName = "Doctor"; 374 }) 375 } 376 } 377} 378``` 379 380- Instances of \@ObservedV2 decorated classes cannot be serialized using **JSON.stringify**. 381 382## Use Scenarios 383 384### Nested Class 385 386In the following example, **Pencil** is the innermost class in the **Son** class. As **Pencil** is decorated by \@ObservedV2 and its **length** property is decorated by \@Trace, changes to **length** can be observed. 387 388The example demonstrates how \@Trace is stacked up against [\@Track](arkts-track.md) and [\@State](arkts-state.md) under the existing state management framework: The @Track decorator offers property-level update capability for classes, but not deep observability; \@State can only observe the changes of the object itself and changes at the first layer; in multi-layer nesting scenarios, you must encapsulate custom components and use [\@Observed](arkts-observed-and-objectlink.md) and [\@ObjectLink](arkts-observed-and-objectlink.md) to observe the changes. 389 390* Click **Button("change length")**, in which **length** is a property decorated by \@Trace. The change of **length** can trigger the re-render of the associated UI component, that is, **UINode (1)**, and output the log "id: 1 renderTimes: x" whose **x** increases according to the number of clicks. 391* Because **son** on the custom component **page** is a regular variable, no change is observed for clicks on **Button("assign Son")**. 392* Clicks on **Button("assign Son")** and **Button("change length")** do not trigger UI re-renders. The reason is that, the change to **son** is not updated to the bound component. 393 394```ts 395@ObservedV2 396class Pencil { 397 @Trace length: number = 21; // If length changes, the bound component is re-rendered. 398} 399class Bag { 400 width: number = 50; 401 height: number = 60; 402 pencil: Pencil = new Pencil(); 403} 404class Son { 405 age: number = 5; 406 school: string = "some"; 407 bag: Bag = new Bag(); 408} 409 410@Entry 411@ComponentV2 412struct Page { 413 son: Son = new Son(); 414 renderTimes: number = 0; 415 isRender(id: number): number { 416 console.info(`id: ${id} renderTimes: ${this.renderTimes}`); 417 this.renderTimes++; 418 return 40; 419 } 420 421 build() { 422 Column() { 423 Text('pencil length'+ this.son.bag.pencil.length) 424 .fontSize(this.isRender(1)) // UINode (1) 425 Button("change length") 426 .onClick(() => { 427 // The value of length is changed upon a click, which triggers a re-render of UINode (1). 428 this.son.bag.pencil.length += 100; 429 }) 430 Button("assign Son") 431 .onClick(() => { 432 // Changes to the regular variable son do not trigger UI re-renders of UINode (1). 433 this.son = new Son(); 434 }) 435 } 436 } 437} 438``` 439 440 441### Inheritance 442 443Properties in base or derived classes are observable only when decorated by \@Trace. 444In the following example, classes **GrandFather**, **Father**, **Uncle**, **Son**, and **Cousin** are declared. The following figure shows the inheritance relationship. 445 446 447 448 449Create instances of the **Son** and **Cousin** classes. Clicks on **Button('change Son age')** and **Button('change Cousin age')** can trigger UI re-renders. 450 451```ts 452@ObservedV2 453class GrandFather { 454 @Trace age: number = 0; 455 456 constructor(age: number) { 457 this.age = age; 458 } 459} 460class Father extends GrandFather{ 461 constructor(father: number) { 462 super(father); 463 } 464} 465class Uncle extends GrandFather { 466 constructor(uncle: number) { 467 super(uncle); 468 } 469} 470class Son extends Father { 471 constructor(son: number) { 472 super(son); 473 } 474} 475class Cousin extends Uncle { 476 constructor(cousin: number) { 477 super(cousin); 478 } 479} 480@Entry 481@ComponentV2 482struct Index { 483 son: Son = new Son(0); 484 cousin: Cousin = new Cousin(0); 485 renderTimes: number = 0; 486 487 isRender(id: number): number { 488 console.info(`id: ${id} renderTimes: ${this.renderTimes}`); 489 this.renderTimes++; 490 return 40; 491 } 492 493 build() { 494 Row() { 495 Column() { 496 Text(`Son ${this.son.age}`) 497 .fontSize(this.isRender(1)) 498 .fontWeight(FontWeight.Bold) 499 Text(`Cousin ${this.cousin.age}`) 500 .fontSize(this.isRender(2)) 501 .fontWeight(FontWeight.Bold) 502 Button('change Son age') 503 .onClick(() => { 504 this.son.age++; 505 }) 506 Button('change Cousin age') 507 .onClick(() => { 508 this.cousin.age++; 509 }) 510 } 511 .width('100%') 512 } 513 .height('100%') 514 } 515} 516``` 517 518### Decorating an Array of a Built-in Type with \@Trace 519 520With an array of a built-in type decorated by \@Trace, changes caused by supported APIs can be observed. For details about the supported APIs, see [Observed Changes](#observed-changes). 521In the following example, the **numberArr** property in the \@ObservedV2 decorated **Arr** class is an \@Trace decorated array. If an array API is used to operate **numberArr**, the change caused can be observed. Perform length checks on arrays to prevent out-of-bounds access. 522 523```ts 524let nextId: number = 0; 525 526@ObservedV2 527class Arr { 528 id: number = 0; 529 @Trace numberArr: number[] = []; 530 531 constructor() { 532 this.id = nextId++; 533 this.numberArr = [0, 1, 2]; 534 } 535} 536 537@Entry 538@ComponentV2 539struct Index { 540 arr: Arr = new Arr(); 541 542 build() { 543 Column() { 544 Text(`length: ${this.arr.numberArr.length}`) 545 .fontSize(40) 546 Divider() 547 if (this.arr.numberArr.length >= 3) { 548 Text(`${this.arr.numberArr[0]}`) 549 .fontSize(40) 550 .onClick(() => { 551 this.arr.numberArr[0]++; 552 }) 553 Text(`${this.arr.numberArr[1]}`) 554 .fontSize(40) 555 .onClick(() => { 556 this.arr.numberArr[1]++; 557 }) 558 Text(`${this.arr.numberArr[2]}`) 559 .fontSize(40) 560 .onClick(() => { 561 this.arr.numberArr[2]++; 562 }) 563 } 564 565 Divider() 566 567 ForEach(this.arr.numberArr, (item: number, index: number) => { 568 Text(`${index} ${item}`) 569 .fontSize(40) 570 }) 571 572 Button('push') 573 .onClick(() => { 574 this.arr.numberArr.push(50); 575 }) 576 577 Button('pop') 578 .onClick(() => { 579 this.arr.numberArr.pop(); 580 }) 581 582 Button('shift') 583 .onClick(() => { 584 this.arr.numberArr.shift(); 585 }) 586 587 Button('splice') 588 .onClick(() => { 589 this.arr.numberArr.splice(1, 0, 60); 590 }) 591 592 593 Button('unshift') 594 .onClick(() => { 595 this.arr.numberArr.unshift(100); 596 }) 597 598 Button('copywithin') 599 .onClick(() => { 600 this.arr.numberArr.copyWithin(0, 1, 2); 601 }) 602 603 Button('fill') 604 .onClick(() => { 605 this.arr.numberArr.fill(0, 2, 4); 606 }) 607 608 Button('reverse') 609 .onClick(() => { 610 this.arr.numberArr.reverse(); 611 }) 612 613 Button('sort') 614 .onClick(() => { 615 this.arr.numberArr.sort(); 616 }) 617 } 618 } 619} 620``` 621 622### Decorating an Object Array with \@Trace 623 624* In the following example, the **personList** object array and the **age** property in the **Person** class are decorated by \@Trace. As such, changes to **personList** and **age** can be observed. 625* Clicking the **Text** component changes the value of **age** and thereby triggers a UI re-render of the **Text** component 626 627```ts 628let nextId: number = 0; 629 630@ObservedV2 631class Person { 632 @Trace age: number = 0; 633 634 constructor(age: number) { 635 this.age = age; 636 } 637} 638 639@ObservedV2 640class Info { 641 id: number = 0; 642 @Trace personList: Person[] = []; 643 644 constructor() { 645 this.id = nextId++; 646 this.personList = [new Person(0), new Person(1), new Person(2)]; 647 } 648} 649 650@Entry 651@ComponentV2 652struct Index { 653 info: Info = new Info(); 654 655 build() { 656 Column() { 657 Text(`length: ${this.info.personList.length}`) 658 .fontSize(40) 659 Divider() 660 if (this.info.personList.length >= 3) { 661 Text(`${this.info.personList[0].age}`) 662 .fontSize(40) 663 .onClick(() => { 664 this.info.personList[0].age++; 665 }) 666 667 Text(`${this.info.personList[1].age}`) 668 .fontSize(40) 669 .onClick(() => { 670 this.info.personList[1].age++; 671 }) 672 673 Text(`${this.info.personList[2].age}`) 674 .fontSize(40) 675 .onClick(() => { 676 this.info.personList[2].age++; 677 }) 678 } 679 680 Divider() 681 682 ForEach(this.info.personList, (item: Person, index: number) => { 683 Text(`${index} ${item.age}`) 684 .fontSize(40) 685 }) 686 } 687 } 688} 689 690``` 691 692### Decorating a Property of the Map Type with \@Trace 693 694* With a property of the Map type decorated by \@Trace, changes caused by supported APIs, such as **set**, **clear**, and **delete**, can be observed. 695* In the following example, the **Info** class is decorated by \@ObservedV2 and its **memberMap** property is decorated by \@Trace; as such, changes to the **memberMap** property caused by clicking **Button('init map')** can be observed. 696 697```ts 698@ObservedV2 699class Info { 700 @Trace memberMap: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]); 701} 702 703@Entry 704@ComponentV2 705struct MapSample { 706 info: Info = new Info(); 707 708 build() { 709 Row() { 710 Column() { 711 ForEach(Array.from(this.info.memberMap.entries()), (item: [number, string]) => { 712 Text(`${item[0]}`) 713 .fontSize(30) 714 Text(`${item[1]}`) 715 .fontSize(30) 716 Divider() 717 }) 718 Button('init map') 719 .onClick(() => { 720 this.info.memberMap = new Map([[0, "a"], [1, "b"], [3, "c"]]); 721 }) 722 Button('set new one') 723 .onClick(() => { 724 this.info.memberMap.set(4, "d"); 725 }) 726 Button('clear') 727 .onClick(() => { 728 this.info.memberMap.clear(); 729 }) 730 Button('set the key: 0') 731 .onClick(() => { 732 this.info.memberMap.set(0, "aa"); 733 }) 734 Button('delete the first one') 735 .onClick(() => { 736 this.info.memberMap.delete(0); 737 }) 738 } 739 .width('100%') 740 } 741 .height('100%') 742 } 743} 744``` 745 746### Decorating a Property of the Set Type with \@Trace 747 748* With a property of the Set type decorated by \@Trace, changes caused by supported APIs, such as **add**, **clear**, and **delete**, can be observed. 749* In the following example, the **Info** class is decorated by \@ObservedV2 and its **memberSet** property is decorated by \@Trace; as such, changes to the **memberSet** property caused by clicking **Button('init set')** can be observed. 750 751```ts 752@ObservedV2 753class Info { 754 @Trace memberSet: Set<number> = new Set([0, 1, 2, 3, 4]); 755} 756 757@Entry 758@ComponentV2 759struct SetSample { 760 info: Info = new Info(); 761 762 build() { 763 Row() { 764 Column() { 765 ForEach(Array.from(this.info.memberSet.entries()), (item: [number, string]) => { 766 Text(`${item[0]}`) 767 .fontSize(30) 768 Divider() 769 }) 770 Button('init set') 771 .onClick(() => { 772 this.info.memberSet = new Set([0, 1, 2, 3, 4]); 773 }) 774 Button('set new one') 775 .onClick(() => { 776 this.info.memberSet.add(5); 777 }) 778 Button('clear') 779 .onClick(() => { 780 this.info.memberSet.clear(); 781 }) 782 Button('delete the first one') 783 .onClick(() => { 784 this.info.memberSet.delete(0); 785 }) 786 } 787 .width('100%') 788 } 789 .height('100%') 790 } 791} 792``` 793 794 795### Decorating a Property of the Date Type with \@Trace 796 797* With a property of the Date type decorated by \@Trace, changes caused by the following APIs can be observed: setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds. 798* In the following example, the **Info** class is decorated by \@ObservedV2 and its **selectedDate** property is decorated by \@Trace; as such, changes to the **selectedDate** property caused by clicking **Button('set selectedDate to 2023-07-08')** can be observed. 799 800```ts 801@ObservedV2 802class Info { 803 @Trace selectedDate: Date = new Date('2021-08-08') 804} 805 806@Entry 807@ComponentV2 808struct DateSample { 809 info: Info = new Info() 810 811 build() { 812 Column() { 813 Button('set selectedDate to 2023-07-08') 814 .margin(10) 815 .onClick(() => { 816 this.info.selectedDate = new Date('2023-07-08'); 817 }) 818 Button('increase the year by 1') 819 .margin(10) 820 .onClick(() => { 821 this.info.selectedDate.setFullYear(this.info.selectedDate.getFullYear() + 1); 822 }) 823 Button('increase the month by 1') 824 .margin(10) 825 .onClick(() => { 826 this.info.selectedDate.setMonth(this.info.selectedDate.getMonth() + 1); 827 }) 828 Button('increase the day by 1') 829 .margin(10) 830 .onClick(() => { 831 this.info.selectedDate.setDate(this.info.selectedDate.getDate() + 1); 832 }) 833 DatePicker({ 834 start: new Date('1970-1-1'), 835 end: new Date('2100-1-1'), 836 selected: this.info.selectedDate 837 }) 838 }.width('100%') 839 } 840} 841``` 842