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