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