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