• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![arkts-old-state-management](figures/arkts-new-observed-and-track-extend-sample.png)
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