• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Proper Use of State Management
2
3Managing state in applications can be a tricky task. You may find the UI not refreshed as expected, or the re-renders slowing down your application. This topic explores some best practices for managing state, through typical correct and incorrect usage examples.
4
5## Properly Using Attributes
6
7### Combining Simple Attributes into Object Arrays
8
9It is commonplace in development to set the same attribute for multiple components, for example, the text content, width, or height attributes. To make these attributes easier to manage, you can store them in an array and use them with **ForEach**.
10
11```typescript
12@Entry
13@Component
14struct Index {
15  @State items: string[] = [];
16  @State ids: string[] = [];
17  @State age: number[] = [];
18  @State gender: string[] = [];
19
20  aboutToAppear() {
21    this.items.push("Head");
22    this.items.push("List");
23    for (let i = 0; i < 20; i++) {
24      this.ids.push("id: " + Math.floor(Math.random() * 1000));
25      this.age.push(Math.floor(Math.random() * 100 % 40));
26      this.gender.push(Math.floor(Math.random() * 100) % 2 == 0 ? "Male" : "Female");
27    }
28  }
29
30  isRenderText(index: number) : number {
31    console.log(`index ${index} is rendered`);
32    return 1;
33  }
34
35  build() {
36    Row() {
37      Column() {
38        ForEach(this.items, (item: string) => {
39          if (item == "Head") {
40            Text("Personal Info")
41              .fontSize(40)
42          } else if (item == "List") {
43            List() {
44              ForEach(this.ids, (id: string, index) => {
45                ListItem() {
46                  Row() {
47                    Text(id)
48                      .fontSize(20)
49                      .margin({
50                        left: 30,
51                        right: 5
52                      })
53                    Text("age: " + this.age[index as number])
54                      .fontSize(20)
55                      .margin({
56                        left: 5,
57                        right: 5
58                      })
59                      .position({x: 100})
60                      .opacity(this.isRenderText(index))
61                      .onClick(() => {
62                        this.age[index]++;
63                      })
64                    Text("gender: " + this.gender[index as number])
65                      .margin({
66                        left: 5,
67                        right: 5
68                      })
69                      .position({x: 180})
70                      .fontSize(20)
71                  }
72                }
73                .margin({
74                  top: 5,
75                  bottom: 5
76                })
77              })
78            }
79          }
80        })
81      }
82    }
83  }
84}
85```
86
87Below you can see how the preceding code snippet works.
88
89![properly-use-state-management-to-develope-1](figures/properly-use-state-management-to-develope-1.gif)
90
91In this example, a total of 20 records are displayed on the page through **ForEach**. When you click the **Text** component of **age** in one of the records, the **Text** components of **age** in other 19 records are also re-rendered - reflected by the logs generated for the components of **age**. However, because the **age** values of the other 19 records do not change, the re-rendering of these records is actually redundant.
92
93This redundant re-rendering is due to a characteristic of state management. Assume that there is an @State decorated number array **Num[]**. This array contains 20 elements whose values are 0 to 19, respectively. Each of the 20 elements is bound to a **Text** component. When one of the elements is changed, all components bound to the elements are re-rendered, regardless of whether the other elements are changed or not.
94
95This seemly bug, commonly known as "redundant re-render", is widely observed in simple array, and can adversely affect the UI re-rendering performance when the arrays are large. To make your rendering process run smoothly, it is crucial to reduce redundant re-renders and update components only when necessary.
96
97In the case of an array of simple attributes, you can avoid redundant re-rendering by converting the array into an object array. The code snippet after optimization is as follows:
98
99```typescript
100@Observed
101class InfoList extends Array<Info> {
102};
103@Observed
104class Info {
105  ids: number;
106  age: number;
107  gender: string;
108
109  constructor() {
110    this.ids = Math.floor(Math.random() * 1000);
111    this.age = Math.floor(Math.random() * 100 % 40);
112    this.gender = Math.floor(Math.random() * 100) % 2 == 0 ? "Male" : "Female";
113  }
114}
115@Component
116struct Information {
117  @ObjectLink info: Info;
118  @State index: number = 0;
119  isRenderText(index: number) : number {
120    console.log(`index ${index} is rendered`);
121    return 1;
122  }
123
124  build() {
125    Row() {
126      Text("id: " + this.info.ids)
127        .fontSize(20)
128        .margin({
129          left: 30,
130          right: 5
131        })
132      Text("age: " + this.info.age)
133        .fontSize(20)
134        .margin({
135          left: 5,
136          right: 5
137        })
138        .position({x: 100})
139        .opacity(this.isRenderText(this.index))
140        .onClick(() => {
141          this.info.age++;
142        })
143      Text("gender: " + this.info.gender)
144        .margin({
145          left: 5,
146          right: 5
147        })
148        .position({x: 180})
149        .fontSize(20)
150    }
151  }
152}
153@Entry
154@Component
155struct Page {
156  @State infoList: InfoList = new InfoList();
157  @State items: string[] = [];
158  aboutToAppear() {
159    this.items.push("Head");
160    this.items.push("List");
161    for (let i = 0; i < 20; i++) {
162      this.infoList.push(new Info());
163    }
164  }
165
166  build() {
167    Row() {
168      Column() {
169        ForEach(this.items, (item: string) => {
170          if (item == "Head") {
171            Text("Personal Info")
172              .fontSize(40)
173          } else if (item == "List") {
174            List() {
175              ForEach(this.infoList, (info: Info, index) => {
176                ListItem() {
177                  Information({
178                    info: info,
179                    index: index
180                  })
181                }
182                .margin({
183                  top: 5,
184                  bottom: 5
185                })
186              })
187            }
188          }
189        })
190      }
191    }
192  }
193}
194```
195
196Below you can see how the preceding code snippet works.
197
198![properly-use-state-management-to-develope-2](figures/properly-use-state-management-to-develope-2.gif)
199
200After optimization, an object array is used in place of the original attribute arrays. For an array, changes in an object cannot be observed and therefore do not cause re-renders. Specifically, only changes at the top level of array items can be observed, for example, adding, modifying, or deleting an item. For a common array, modifying a data item means to change the item's value. For an object array, it means to assign a new value to the entire object, which means that changes to a property in an object are not observable to the array and consequently do not cause a re-render. In addition to property changes in object arrays, changes in nested objects cannot be observed either, which is further detailed in [Splitting a Complex Large Object into Multiple Small Objects](#splitting-a-complex-large-object-into-multiple-small-objects). In the code after optimization, you may notice a combination of custom components and **ForEach**. For details, see [Using Custom Components to Match Object Arrays in ForEach](#using-custom-components-to-match-object-arrays-in-foreach).
201
202### Splitting a Complex Large Object into Multiple Small Objects
203
204> **NOTE**
205>
206> You are advised to use the [@Track](arkts-track.md) decorator in this scenario since API version 11.
207
208During development, we sometimes define a large object that contains many style-related properties, and pass the object between parent and child components to bind the properties to the components.
209
210```typescript
211@Observed
212class UIStyle {
213  translateX: number = 0;
214  translateY: number = 0;
215  scaleX: number = 0.3;
216  scaleY: number = 0.3;
217  width: number = 336;
218  height: number = 178;
219  posX: number = 10;
220  posY: number = 50;
221  alpha: number = 0.5;
222  borderRadius: number = 24;
223  imageWidth: number = 78;
224  imageHeight: number = 78;
225  translateImageX: number = 0;
226  translateImageY: number = 0;
227  fontSize: number = 20;
228}
229@Component
230struct SpecialImage {
231  @ObjectLink uiStyle: UIStyle;
232  private isRenderSpecialImage() : number { // A function indicating whether the component is rendered.
233    console.log("SpecialImage is rendered");
234    return 1;
235  }
236  build() {
237    Image($r('app.media.icon')) // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
238      .width(this.uiStyle.imageWidth)
239      .height(this.uiStyle.imageHeight)
240      .margin({ top: 20 })
241      .translate({
242        x: this.uiStyle.translateImageX,
243        y: this.uiStyle.translateImageY
244      })
245      .opacity(this.isRenderSpecialImage()) // If the image is re-rendered, this function will be called.
246  }
247}
248@Component
249struct PageChild {
250  @ObjectLink uiStyle: UIStyle
251  // The following function is used to display whether the component is rendered.
252  private isRenderColumn() : number {
253    console.log("Column is rendered");
254    return 1;
255  }
256  private isRenderStack() : number {
257    console.log("Stack is rendered");
258    return 1;
259  }
260  private isRenderImage() : number {
261    console.log("Image is rendered");
262    return 1;
263  }
264  private isRenderText() : number {
265    console.log("Text is rendered");
266    return 1;
267  }
268  build() {
269    Column() {
270      SpecialImage({
271        uiStyle: this.uiStyle
272      })
273      Stack() {
274        Column() {
275            Image($r('app.media.icon')) // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
276              .opacity(this.uiStyle.alpha)
277              .scale({
278                x: this.uiStyle.scaleX,
279                y: this.uiStyle.scaleY
280              })
281              .padding(this.isRenderImage())
282              .width(300)
283              .height(300)
284        }
285        .width('100%')
286        .position({ y: -80 })
287        Stack() {
288          Text("Hello World")
289            .fontColor("#182431")
290            .fontWeight(FontWeight.Medium)
291            .fontSize(this.uiStyle.fontSize)
292            .opacity(this.isRenderText())
293            .margin({ top: 12 })
294        }
295        .opacity(this.isRenderStack())
296        .position({
297          x: this.uiStyle.posX,
298          y: this.uiStyle.posY
299        })
300        .width('100%')
301        .height('100%')
302      }
303      .margin({ top: 50 })
304      .borderRadius(this.uiStyle.borderRadius)
305      .opacity(this.isRenderStack())
306      .backgroundColor("#FFFFFF")
307      .width(this.uiStyle.width)
308      .height(this.uiStyle.height)
309      .translate({
310        x: this.uiStyle.translateX,
311        y: this.uiStyle.translateY
312      })
313      Column() {
314        Button("Move")
315          .width(312)
316          .fontSize(20)
317          .backgroundColor("#FF007DFF")
318          .margin({ bottom: 10 })
319          .onClick(() => {
320            animateTo({
321              duration: 500
322            },() => {
323              this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250;
324            })
325          })
326        Button("Scale")
327          .borderRadius(20)
328          .backgroundColor("#FF007DFF")
329          .fontSize(20)
330          .width(312)
331          .onClick(() => {
332            this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8;
333          })
334      }
335      .position({
336        y:666
337      })
338      .height('100%')
339      .width('100%')
340
341    }
342    .opacity(this.isRenderColumn())
343    .width('100%')
344    .height('100%')
345
346  }
347}
348@Entry
349@Component
350struct Page {
351  @State uiStyle: UIStyle = new UIStyle();
352  build() {
353    Stack() {
354      PageChild({
355        uiStyle: this.uiStyle
356      })
357    }
358    .backgroundColor("#F1F3F5")
359  }
360}
361```
362
363Below you can see how the preceding code snippet works.
364
365![properly-use-state-management-to-develope-3](figures/properly-use-state-management-to-develope-3.gif)
366
367Click the **Move** button before optimization. The duration for updating dirty nodes is as follows.
368
369![img](figures/properly-use-state-management-to-develope-11.PNG)
370
371In the above example, **uiStyle** defines multiple properties, which are each associated with multiple components. When some of these properties are changed at the click of a button, all the components associated with **uiStyle** are re-rendered, even though they do not need to (because the properties of these components are not changed). The re-renders of these components can be observed through a series of defined **isRender** functions. When **Move** is clicked to perform the translation animation, the value of **translateY** changes multiple times. As a result, redundant re-renders occur at each frame, which greatly worsen the application performance.
372
373Such redundant re-renders result from an update mechanism of the state management: If multiple properties of a class are bound to different components through an object of the class, then, if any of the properties is changed, the component associated with the property is re-rendered, together with components associated with the other properties, even though the other properties do not change.
374
375Naturally, this update mechanism brings down the re-rendering performance, especially in the case of a large, complex object associated with a considerable number of components. To fix this issue, split a large, complex object into a set of multiple small objects. In this way, redundant re-renders are reduced and the render scope precisely controlled, while the original code structure is retained.
376
377```typescript
378@Observed
379class NeedRenderImage { // Properties used in the same component can be classified into the same class.
380  public translateImageX: number = 0;
381  public translateImageY: number = 0;
382  public imageWidth:number = 78;
383  public imageHeight:number = 78;
384}
385@Observed
386class NeedRenderScale { // Properties used together can be classified into the same class.
387  public scaleX: number = 0.3;
388  public scaleY: number = 0.3;
389}
390@Observed
391class NeedRenderAlpha { // Properties used separately can be classified into the same class.
392  public alpha: number = 0.5;
393}
394@Observed
395class NeedRenderSize { // Properties used together can be classified into the same class.
396  public width: number = 336;
397  public height: number = 178;
398}
399@Observed
400class NeedRenderPos { // Properties used together can be classified into the same class.
401  public posX: number = 10;
402  public posY: number = 50;
403}
404@Observed
405class NeedRenderBorderRadius { // Properties used separately can be classified into the same class.
406  public borderRadius: number = 24;
407}
408@Observed
409class NeedRenderFontSize { // Properties used separately can be classified into the same class.
410  public fontSize: number = 20;
411}
412@Observed
413class NeedRenderTranslate { // Properties used together can be classified into the same class.
414  public translateX: number = 0;
415  public translateY: number = 0;
416}
417@Observed
418class UIStyle {
419  // Use the NeedRenderxxx class.
420  needRenderTranslate: NeedRenderTranslate = new NeedRenderTranslate();
421  needRenderFontSize: NeedRenderFontSize = new NeedRenderFontSize();
422  needRenderBorderRadius: NeedRenderBorderRadius = new NeedRenderBorderRadius();
423  needRenderPos: NeedRenderPos = new NeedRenderPos();
424  needRenderSize: NeedRenderSize = new NeedRenderSize();
425  needRenderAlpha: NeedRenderAlpha = new NeedRenderAlpha();
426  needRenderScale: NeedRenderScale = new NeedRenderScale();
427  needRenderImage: NeedRenderImage = new NeedRenderImage();
428}
429@Component
430struct SpecialImage {
431  @ObjectLink uiStyle : UIStyle;
432  @ObjectLink needRenderImage: NeedRenderImage // Receive a new class from its parent component.
433  private isRenderSpecialImage() : number { // A function indicating whether the component is rendered.
434    console.log("SpecialImage is rendered");
435    return 1;
436  }
437  build() {
438    Image($r('app.media.icon')) // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
439      .width(this.needRenderImage.imageWidth) // Use this.needRenderImage.xxx.
440      .height(this.needRenderImage.imageHeight)
441      .margin({top:20})
442      .translate({
443        x: this.needRenderImage.translateImageX,
444        y: this.needRenderImage.translateImageY
445      })
446      .opacity(this.isRenderSpecialImage()) // If the image is re-rendered, this function will be called.
447  }
448}
449@Component
450struct PageChild {
451  @ObjectLink uiStyle: UIStyle;
452  @ObjectLink needRenderTranslate: NeedRenderTranslate; // Receive the newly defined instance of the NeedRenderxxx class from its parent component.
453  @ObjectLink needRenderFontSize: NeedRenderFontSize;
454  @ObjectLink needRenderBorderRadius: NeedRenderBorderRadius;
455  @ObjectLink needRenderPos: NeedRenderPos;
456  @ObjectLink needRenderSize: NeedRenderSize;
457  @ObjectLink needRenderAlpha: NeedRenderAlpha;
458  @ObjectLink needRenderScale: NeedRenderScale;
459  // The following function is used to display whether the component is rendered.
460  private isRenderColumn() : number {
461    console.log("Column is rendered");
462    return 1;
463  }
464  private isRenderStack() : number {
465    console.log("Stack is rendered");
466    return 1;
467  }
468  private isRenderImage() : number {
469    console.log("Image is rendered");
470    return 1;
471  }
472  private isRenderText() : number {
473    console.log("Text is rendered");
474    return 1;
475  }
476  build() {
477    Column() {
478      SpecialImage({
479        uiStyle: this.uiStyle,
480        needRenderImage: this.uiStyle.needRenderImage // Pass the needRenderxxx class to the child component.
481      })
482      Stack() {
483        Column() {
484          Image($r('app.media.icon')) // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
485            .opacity(this.needRenderAlpha.alpha)
486            .scale({
487              x: this.needRenderScale.scaleX, // Use this.needRenderXxx.xxx.
488              y: this.needRenderScale.scaleY
489            })
490            .padding(this.isRenderImage())
491            .width(300)
492            .height(300)
493        }
494        .width('100%')
495        .position({ y: -80 })
496
497        Stack() {
498          Text("Hello World")
499            .fontColor("#182431")
500            .fontWeight(FontWeight.Medium)
501            .fontSize(this.needRenderFontSize.fontSize)
502            .opacity(this.isRenderText())
503            .margin({ top: 12 })
504        }
505        .opacity(this.isRenderStack())
506        .position({
507          x: this.needRenderPos.posX,
508          y: this.needRenderPos.posY
509        })
510        .width('100%')
511        .height('100%')
512      }
513      .margin({ top: 50 })
514      .borderRadius(this.needRenderBorderRadius.borderRadius)
515      .opacity(this.isRenderStack())
516      .backgroundColor("#FFFFFF")
517      .width(this.needRenderSize.width)
518      .height(this.needRenderSize.height)
519      .translate({
520        x: this.needRenderTranslate.translateX,
521        y: this.needRenderTranslate.translateY
522      })
523
524      Column() {
525        Button("Move")
526          .width(312)
527          .fontSize(20)
528          .backgroundColor("#FF007DFF")
529          .margin({ bottom: 10 })
530          .onClick(() => {
531            animateTo({
532              duration: 500
533            }, () => {
534              this.needRenderTranslate.translateY = (this.needRenderTranslate.translateY + 180) % 250;
535            })
536          })
537        Button("Scale")
538          .borderRadius(20)
539          .backgroundColor("#FF007DFF")
540          .fontSize(20)
541          .width(312)
542          .margin({ bottom: 10 })
543          .onClick(() => {
544            this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 0.8;
545          })
546        Button("Change Image")
547          .borderRadius(20)
548          .backgroundColor("#FF007DFF")
549          .fontSize(20)
550          .width(312)
551          .onClick(() => { // Use this.uiStyle.endRenderXxx.xxx to change the property in the parent component.
552            this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 30) % 160;
553            this.uiStyle.needRenderImage.imageHeight = (this.uiStyle.needRenderImage.imageHeight + 30) % 160;
554          })
555      }
556      .position({
557        y: 616
558      })
559      .height('100%')
560      .width('100%')
561    }
562    .opacity(this.isRenderColumn())
563    .width('100%')
564    .height('100%')
565  }
566}
567@Entry
568@Component
569struct Page {
570  @State uiStyle: UIStyle = new UIStyle();
571  build() {
572    Stack() {
573      PageChild({
574        uiStyle: this.uiStyle,
575        needRenderTranslate: this.uiStyle.needRenderTranslate, // Pass the needRenderxxx class to the child component.
576        needRenderFontSize: this.uiStyle.needRenderFontSize,
577        needRenderBorderRadius: this.uiStyle.needRenderBorderRadius,
578        needRenderPos: this.uiStyle.needRenderPos,
579        needRenderSize: this.uiStyle.needRenderSize,
580        needRenderAlpha: this.uiStyle.needRenderAlpha,
581        needRenderScale: this.uiStyle.needRenderScale
582      })
583    }
584    .backgroundColor("#F1F3F5")
585  }
586}
587```
588
589Below you can see how the preceding code snippet works.![properly-use-state-management-to-develope-4](figures/properly-use-state-management-to-develope-4.gif)
590
591Click the **Move** button after optimization. The duration for updating dirty nodes is as follows.
592
593![img](figures/properly-use-state-management-to-develope-12.PNG)
594
595After the optimization, the 15 attributes previously in one class are divided into eight classes, and the bound components are adapted accordingly. The division of properties complies with the following principles:
596
597- Properties that are only used in the same component can be divided into the same new child class, that is, **NeedRenderImage** in the example. This mode of division is applicable to the scenario where components are frequently re-rendered due to changes of unassociated properties.
598- Properties that are frequently used together can be divided into the same new child class, that is, **NeedRenderScale**, **NeedRenderTranslate**, **NeedRenderPos**, and **NeedRenderSize** in the example. This mode of division is applicable to the scenario where properties often appear in pairs or are applied to the same style, for example, **.translate**, **.position**, and **.scale** (which usually receive an object as a parameter).
599- Properties that may be used in different places should be divided into a new child class, that is, **NeedRenderAlpha**, **NeedRenderBorderRadius**, and **NeedRenderFontSize** in the example. This mode of division is applicable to the scenario where a property works on multiple components or works on their own, for example, **.opacity** and **.borderRadius** (which usually work on their own).
600
601As in combination of properties, the principle behind division of properties is that changes to properties of objects nested more than two levels deep cannot be observed. Yet, you can use @Observed and @ObjectLink to transfer level-2 objects between parent and child nodes to observe property changes at level 2 and precisely control the render scope. <!--Del-->For details about the division of properties, see [Precisely Controlling Render Scope](https://gitee.com/openharmony/docs/blob/master/en/application-dev/performance/precisely-control-render-scope.md).<!--DelEnd-->
602
603@Track decorator can also precisely control the render scope, which does not involve division of properties.
604
605```ts
606@Observed
607class UIStyle {
608  @Track translateX: number = 0;
609  @Track translateY: number = 0;
610  @Track scaleX: number = 0.3;
611  @Track scaleY: number = 0.3;
612  @Track width: number = 336;
613  @Track height: number = 178;
614  @Track posX: number = 10;
615  @Track posY: number = 50;
616  @Track alpha: number = 0.5;
617  @Track borderRadius: number = 24;
618  @Track imageWidth: number = 78;
619  @Track imageHeight: number = 78;
620  @Track translateImageX: number = 0;
621  @Track translateImageY: number = 0;
622  @Track fontSize: number = 20;
623}
624@Component
625struct SpecialImage {
626  @ObjectLink uiStyle: UIStyle;
627  private isRenderSpecialImage() : number { // A function indicating whether the component is rendered.
628    console.log("SpecialImage is rendered");
629    return 1;
630  }
631  build() {
632    Image($r('app.media.icon')) // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
633      .width(this.uiStyle.imageWidth)
634      .height(this.uiStyle.imageHeight)
635      .margin({ top: 20 })
636      .translate({
637        x: this.uiStyle.translateImageX,
638        y: this.uiStyle.translateImageY
639      })
640      .opacity(this.isRenderSpecialImage()) // If the image is re-rendered, this function will be called.
641  }
642}
643@Component
644struct PageChild {
645  @ObjectLink uiStyle: UIStyle
646  // The following function is used to display whether the component is rendered.
647  private isRenderColumn() : number {
648    console.log("Column is rendered");
649    return 1;
650  }
651  private isRenderStack() : number {
652    console.log("Stack is rendered");
653    return 1;
654  }
655  private isRenderImage() : number {
656    console.log("Image is rendered");
657    return 1;
658  }
659  private isRenderText() : number {
660    console.log("Text is rendered");
661    return 1;
662  }
663  build() {
664    Column() {
665      SpecialImage({
666        uiStyle: this.uiStyle
667      })
668      Stack() {
669        Column() {
670            Image($r('app.media.icon')) // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
671              .opacity(this.uiStyle.alpha)
672              .scale({
673                x: this.uiStyle.scaleX,
674                y: this.uiStyle.scaleY
675              })
676              .padding(this.isRenderImage())
677              .width(300)
678              .height(300)
679        }
680        .width('100%')
681        .position({ y: -80 })
682        Stack() {
683          Text("Hello World")
684            .fontColor("#182431")
685            .fontWeight(FontWeight.Medium)
686            .fontSize(this.uiStyle.fontSize)
687            .opacity(this.isRenderText())
688            .margin({ top: 12 })
689        }
690        .opacity(this.isRenderStack())
691        .position({
692          x: this.uiStyle.posX,
693          y: this.uiStyle.posY
694        })
695        .width('100%')
696        .height('100%')
697      }
698      .margin({ top: 50 })
699      .borderRadius(this.uiStyle.borderRadius)
700      .opacity(this.isRenderStack())
701      .backgroundColor("#FFFFFF")
702      .width(this.uiStyle.width)
703      .height(this.uiStyle.height)
704      .translate({
705        x: this.uiStyle.translateX,
706        y: this.uiStyle.translateY
707      })
708      Column() {
709        Button("Move")
710          .width(312)
711          .fontSize(20)
712          .backgroundColor("#FF007DFF")
713          .margin({ bottom: 10 })
714          .onClick(() => {
715            animateTo({
716              duration: 500
717            },() => {
718              this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250;
719            })
720          })
721        Button("Scale")
722          .borderRadius(20)
723          .backgroundColor("#FF007DFF")
724          .fontSize(20)
725          .width(312)
726          .onClick(() => {
727            this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8;
728          })
729      }
730      .position({
731        y:666
732      })
733      .height('100%')
734      .width('100%')
735
736    }
737    .opacity(this.isRenderColumn())
738    .width('100%')
739    .height('100%')
740
741  }
742}
743@Entry
744@Component
745struct Page {
746  @State uiStyle: UIStyle = new UIStyle();
747  build() {
748    Stack() {
749      PageChild({
750        uiStyle: this.uiStyle
751      })
752    }
753    .backgroundColor("#F1F3F5")
754  }
755}
756```
757
758
759
760### Binding Components to Class Objects Decorated with @Observed or Declared as State Variables
761
762Your application may sometimes allow users to reset data - by assigning a new object to the target state variable. The type of the new object is the trick here: If not handled carefully, it may result in the UI not being re-rendered as expected.
763
764```typescript
765@Observed
766class Child {
767  count: number;
768  constructor(count: number) {
769    this.count = count
770  }
771}
772@Observed
773class ChildList extends Array<Child> {
774};
775@Observed
776class Ancestor {
777  childList: ChildList;
778  constructor(childList: ChildList) {
779    this.childList = childList;
780  }
781  public loadData() {
782    let tempList = [new Child(1), new Child(2), new Child(3), new Child(4), new Child(5)];
783    this.childList = tempList;
784  }
785
786  public clearData() {
787    this.childList = []
788  }
789}
790@Component
791struct CompChild {
792  @Link childList: ChildList;
793  @ObjectLink child: Child;
794
795  build() {
796    Row() {
797      Text(this.child.count+'')
798        .height(70)
799        .fontSize(20)
800        .borderRadius({
801          topLeft: 6,
802          topRight: 6
803        })
804        .margin({left: 50})
805      Button('X')
806        .backgroundColor(Color.Red)
807        .onClick(()=>{
808          let index = this.childList.findIndex((item) => {
809            return item.count === this.child.count
810          })
811          if (index !== -1) {
812            this.childList.splice(index, 1);
813          }
814        })
815        .margin({
816          left: 200,
817          right:30
818        })
819    }
820    .margin({
821      top:15,
822      left: 15,
823      right:10,
824      bottom:15
825    })
826    .borderRadius(6)
827    .backgroundColor(Color.Grey)
828  }
829}
830@Component
831struct CompList {
832  @ObjectLink@Watch('changeChildList') childList: ChildList;
833
834  changeChildList() {
835    console.log('CompList ChildList change');
836  }
837
838  isRenderCompChild(index: number) : number {
839    console.log("Comp Child is render" + index);
840    return 1;
841  }
842
843  build() {
844    Column() {
845      List() {
846        ForEach(this.childList, (item: Child, index) => {
847          ListItem() {
848            CompChild({
849              childList: this.childList,
850              child: item
851            })
852              .opacity(this.isRenderCompChild(index))
853          }
854
855        })
856      }
857      .height('70%')
858    }
859  }
860}
861@Component
862struct CompAncestor {
863  @ObjectLink ancestor: Ancestor;
864
865  build() {
866    Column() {
867      CompList({ childList: this.ancestor.childList })
868      Row() {
869        Button("Clear")
870          .onClick(() => {
871            this.ancestor.clearData()
872          })
873          .width(100)
874          .margin({right: 50})
875        Button("Recover")
876          .onClick(() => {
877            this.ancestor.loadData()
878          })
879          .width(100)
880      }
881    }
882  }
883}
884@Entry
885@Component
886struct Page {
887  @State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)];
888  @State ancestor: Ancestor = new Ancestor(this.childList)
889
890  build() {
891    Column() {
892      CompAncestor({ ancestor: this.ancestor})
893    }
894  }
895}
896```
897
898Below you can see how the preceding code snippet works.
899
900![properly-use-state-management-to-develope-5](figures/properly-use-state-management-to-develope-5.gif)
901
902In the code there is a data source of the ChildList type. If you click **X** to delete some data and then click **Recover** to restore **ChildList**, the UI is not re-rendered after you click **X** again, and no "CompList ChildList change" log is printed.
903
904An examination of the code finds out that when a value is re-assigned to the data source **ChildList** through the **loadData** method of the **Ancestor** object.
905
906```typescript
907  public loadData() {
908    let tempList = [new Child(1), new Child(2), new Child(3), new Child(4), new Child(5)];
909    this.childList = tempList;
910  }
911```
912
913In the **loadData** method, **tempList**, a temporary array of the Child type, is created, to which the member variable **ChildList** of the **Ancestor** object is pointed. However, value changes of the **tempList** array cannot be observed. In other words, its value changes do not cause UI re-renders. After the array is assigned to **childList**, the **ForEach** view is updated and the UI is re-rendered. When you click **X** again, however, the UI is not re-rendered to reflect the decrease in **childList**, because **childList** points to a new, unobservable **tempList**.
914
915You may notice that **childList** is initialized in the same way when it is defined in **Page**.
916
917```typescript
918@State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)];
919@State ancestor: Ancestor = new Ancestor(this.childList)
920```
921
922Yet, **childList** there is observable, being decorated by @State. As such, while it is assigned an array of the Child[] type not decorated by @Observed, its value changes can cause UI re-renders. If the @State decorator is removed from **childList**, the data source is not reset and UI re-renders cannot be triggered by clicking the **X** button.
923
924In summary, for the UI to be re-rendered properly upon value changes of class objects, these class objects must be observable.
925
926```typescript
927@Observed
928class Child {
929  count: number;
930  constructor(count: number) {
931    this.count = count
932  }
933}
934@Observed
935class ChildList extends Array<Child> {
936};
937@Observed
938class Ancestor {
939  childList: ChildList;
940  constructor(childList: ChildList) {
941    this.childList = childList;
942  }
943  public loadData() {
944    let tempList = new ChildList();
945    for (let i = 1; i < 6; i ++) {
946      tempList.push(new Child(i));
947    }
948    this.childList = tempList;
949  }
950
951  public clearData() {
952    this.childList = []
953  }
954}
955@Component
956struct CompChild {
957  @Link childList: ChildList;
958  @ObjectLink child: Child;
959
960  build() {
961    Row() {
962      Text(this.child.count+'')
963        .height(70)
964        .fontSize(20)
965        .borderRadius({
966          topLeft: 6,
967          topRight: 6
968        })
969        .margin({left: 50})
970      Button('X')
971        .backgroundColor(Color.Red)
972        .onClick(()=>{
973          let index = this.childList.findIndex((item) => {
974            return item.count === this.child.count
975          })
976          if (index !== -1) {
977            this.childList.splice(index, 1);
978          }
979        })
980        .margin({
981          left: 200,
982          right:30
983        })
984    }
985    .margin({
986      top:15,
987      left: 15,
988      right:10,
989      bottom:15
990    })
991    .borderRadius(6)
992    .backgroundColor(Color.Grey)
993  }
994}
995@Component
996struct CompList {
997  @ObjectLink@Watch('changeChildList') childList: ChildList;
998
999  changeChildList() {
1000    console.log('CompList ChildList change');
1001  }
1002
1003  isRenderCompChild(index: number) : number {
1004    console.log("Comp Child is render" + index);
1005    return 1;
1006  }
1007
1008  build() {
1009    Column() {
1010      List() {
1011        ForEach(this.childList, (item: Child, index) => {
1012          ListItem() {
1013            CompChild({
1014              childList: this.childList,
1015              child: item
1016            })
1017              .opacity(this.isRenderCompChild(index))
1018          }
1019
1020        })
1021      }
1022      .height('70%')
1023    }
1024  }
1025}
1026@Component
1027struct CompAncestor {
1028  @ObjectLink ancestor: Ancestor;
1029
1030  build() {
1031    Column() {
1032      CompList({ childList: this.ancestor.childList })
1033      Row() {
1034        Button("Clear")
1035          .onClick(() => {
1036            this.ancestor.clearData()
1037          })
1038          .width(100)
1039          .margin({right: 50})
1040        Button("Recover")
1041          .onClick(() => {
1042            this.ancestor.loadData()
1043          })
1044          .width(100)
1045      }
1046    }
1047  }
1048}
1049@Entry
1050@Component
1051struct Page {
1052  @State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)];
1053  @State ancestor: Ancestor = new Ancestor(this.childList)
1054
1055  build() {
1056    Column() {
1057      CompAncestor({ ancestor: this.ancestor})
1058    }
1059  }
1060}
1061```
1062
1063Below you can see how the preceding code snippet works.
1064
1065![properly-use-state-management-to-develope-6](figures/properly-use-state-management-to-develope-6.gif)
1066
1067The core of optimization is to change **tempList** of the Child[] type to an observable **ChildList** class.
1068
1069```typescript
1070public loadData() {
1071    let tempList = new ChildList();
1072    for (let i = 1; i < 6; i ++) {
1073      tempList.push(new Child(i));
1074    }
1075    this.childList = tempList;
1076  }
1077```
1078
1079In the preceding code, the ChildList type is decorated by @Observed when defined, allowing the **tempList** object created using **new** to be observed. As such, when you click **X** to delete an item, this change to **childList** is observed, the **ForEach** view updated, and the UI re-rendered.
1080
1081## Properly Using ForEach and LazyForEach
1082
1083### Minimizing the Use of LazyForEach in UI Updating
1084
1085[LazyForEach](arkts-rendering-control-lazyforeach.md) often works hand in hand with state variables.
1086
1087```typescript
1088class BasicDataSource implements IDataSource {
1089  private listeners: DataChangeListener[] = [];
1090  private originDataArray: StringData[] = [];
1091
1092  public totalCount(): number {
1093    return 0;
1094  }
1095
1096  public getData(index: number): StringData {
1097    return this.originDataArray[index];
1098  }
1099
1100  registerDataChangeListener(listener: DataChangeListener): void {
1101    if (this.listeners.indexOf(listener) < 0) {
1102      console.info('add listener');
1103      this.listeners.push(listener);
1104    }
1105  }
1106
1107  unregisterDataChangeListener(listener: DataChangeListener): void {
1108    const pos = this.listeners.indexOf(listener);
1109    if (pos >= 0) {
1110      console.info('remove listener');
1111      this.listeners.splice(pos, 1);
1112    }
1113  }
1114
1115  notifyDataReload(): void {
1116    this.listeners.forEach(listener => {
1117      listener.onDataReloaded();
1118    })
1119  }
1120
1121  notifyDataAdd(index: number): void {
1122    this.listeners.forEach(listener => {
1123      listener.onDataAdd(index);
1124    })
1125  }
1126
1127  notifyDataChange(index: number): void {
1128    this.listeners.forEach(listener => {
1129      listener.onDataChange(index);
1130    })
1131  }
1132
1133  notifyDataDelete(index: number): void {
1134    this.listeners.forEach(listener => {
1135      listener.onDataDelete(index);
1136    })
1137  }
1138
1139  notifyDataMove(from: number, to: number): void {
1140    this.listeners.forEach(listener => {
1141      listener.onDataMove(from, to);
1142    })
1143  }
1144}
1145
1146class MyDataSource extends BasicDataSource {
1147  private dataArray: StringData[] = [];
1148
1149  public totalCount(): number {
1150    return this.dataArray.length;
1151  }
1152
1153  public getData(index: number): StringData {
1154    return this.dataArray[index];
1155  }
1156
1157  public addData(index: number, data: StringData): void {
1158    this.dataArray.splice(index, 0, data);
1159    this.notifyDataAdd(index);
1160  }
1161
1162  public pushData(data: StringData): void {
1163    this.dataArray.push(data);
1164    this.notifyDataAdd(this.dataArray.length - 1);
1165  }
1166
1167  public reloadData(): void {
1168    this.notifyDataReload();
1169  }
1170}
1171
1172class StringData {
1173  message: string;
1174  imgSrc: Resource;
1175  constructor(message: string, imgSrc: Resource) {
1176    this.message = message;
1177    this.imgSrc = imgSrc;
1178  }
1179}
1180
1181@Entry
1182@Component
1183struct MyComponent {
1184  private data: MyDataSource = new MyDataSource();
1185
1186  aboutToAppear() {
1187    for (let i = 0; i <= 9; i++) {
1188      // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
1189      this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon')));
1190    }
1191  }
1192
1193  build() {
1194    List({ space: 3 }) {
1195      LazyForEach(this.data, (item: StringData, index: number) => {
1196        ListItem() {
1197          Column() {
1198            Text(item.message).fontSize(20)
1199              .onAppear(() => {
1200                console.info("text appear:" + item.message);
1201              })
1202            Image(item.imgSrc)
1203              .width(100)
1204              .height(100)
1205              .onAppear(() => {
1206                console.info("image appear");
1207              })
1208          }.margin({ left: 10, right: 10 })
1209        }
1210        .onClick(() => {
1211          item.message += '0';
1212          this.data.reloadData();
1213        })
1214      }, (item: StringData, index: number) => JSON.stringify(item))
1215    }.cachedCount(5)
1216  }
1217}
1218```
1219
1220Below you can see how the preceding code snippet works.
1221
1222![properly-use-state-management-to-develope-7](figures/properly-use-state-management-to-develope-7.gif)
1223
1224In this example, after you click to change **message**, the image flickers, and the onAppear log is generated for the image, indicating that the component is rebuilt. After **message** is changed, the key of the corresponding list item in **LazyForEach** changes. As a result, **LazyForEach** rebuilds the list item when executing **reloadData**. Though the **Text** component only has its content changed, it is rebuilt, not updated. The **Image** component under the list item is also rebuilt along with the list item, even though its content remains unchanged.
1225
1226While both **LazyForEach** and state variables can trigger UI re-renders, their performance overheads are different. **LazyForEach** leads to component rebuilds and higher performance overheads, especially when there is a considerable number of components. By contrast, the use of state variables allows you to keep the update scope within the closely related components. In light of this, it is recommended that you use state variables to trigger component updates in **LazyForEach**, which requires custom components.
1227
1228```typescript
1229class BasicDataSource implements IDataSource {
1230  private listeners: DataChangeListener[] = [];
1231  private originDataArray: StringData[] = [];
1232
1233  public totalCount(): number {
1234    return 0;
1235  }
1236
1237  public getData(index: number): StringData {
1238    return this.originDataArray[index];
1239  }
1240
1241  registerDataChangeListener(listener: DataChangeListener): void {
1242    if (this.listeners.indexOf(listener) < 0) {
1243      console.info('add listener');
1244      this.listeners.push(listener);
1245    }
1246  }
1247
1248  unregisterDataChangeListener(listener: DataChangeListener): void {
1249    const pos = this.listeners.indexOf(listener);
1250    if (pos >= 0) {
1251      console.info('remove listener');
1252      this.listeners.splice(pos, 1);
1253    }
1254  }
1255
1256  notifyDataReload(): void {
1257    this.listeners.forEach(listener => {
1258      listener.onDataReloaded();
1259    })
1260  }
1261
1262  notifyDataAdd(index: number): void {
1263    this.listeners.forEach(listener => {
1264      listener.onDataAdd(index);
1265    })
1266  }
1267
1268  notifyDataChange(index: number): void {
1269    this.listeners.forEach(listener => {
1270      listener.onDataChange(index);
1271    })
1272  }
1273
1274  notifyDataDelete(index: number): void {
1275    this.listeners.forEach(listener => {
1276      listener.onDataDelete(index);
1277    })
1278  }
1279
1280  notifyDataMove(from: number, to: number): void {
1281    this.listeners.forEach(listener => {
1282      listener.onDataMove(from, to);
1283    })
1284  }
1285}
1286
1287class MyDataSource extends BasicDataSource {
1288  private dataArray: StringData[] = [];
1289
1290  public totalCount(): number {
1291    return this.dataArray.length;
1292  }
1293
1294  public getData(index: number): StringData {
1295    return this.dataArray[index];
1296  }
1297
1298  public addData(index: number, data: StringData): void {
1299    this.dataArray.splice(index, 0, data);
1300    this.notifyDataAdd(index);
1301  }
1302
1303  public pushData(data: StringData): void {
1304    this.dataArray.push(data);
1305    this.notifyDataAdd(this.dataArray.length - 1);
1306  }
1307}
1308
1309@Observed
1310class StringData {
1311  @Track message: string;
1312  @Track imgSrc: Resource;
1313  constructor(message: string, imgSrc: Resource) {
1314    this.message = message;
1315    this.imgSrc = imgSrc;
1316  }
1317}
1318
1319@Entry
1320@Component
1321struct MyComponent {
1322  @State data: MyDataSource = new MyDataSource();
1323
1324  aboutToAppear() {
1325    for (let i = 0; i <= 9; i++) {
1326      // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
1327      this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon')));
1328    }
1329  }
1330
1331  build() {
1332    List({ space: 3 }) {
1333      LazyForEach(this.data, (item: StringData, index: number) => {
1334        ListItem() {
1335          ChildComponent({data: item})
1336        }
1337        .onClick(() => {
1338          item.message += '0';
1339        })
1340      }, (item: StringData, index: number) => index.toString())
1341    }.cachedCount(5)
1342  }
1343}
1344
1345@Component
1346struct ChildComponent {
1347  @ObjectLink data: StringData
1348  build() {
1349    Column() {
1350      Text(this.data.message).fontSize(20)
1351        .onAppear(() => {
1352          console.info("text appear:" + this.data.message)
1353        })
1354      Image(this.data.imgSrc)
1355        .width(100)
1356        .height(100)
1357    }.margin({ left: 10, right: 10 })
1358  }
1359}
1360```
1361
1362Below you can see how the preceding code snippet works.
1363
1364![properly-use-state-management-to-develope-8](figures/properly-use-state-management-to-develope-8.gif)
1365
1366In this example, the UI is re-rendered properly: The image does not flicker, and no log is generated, which indicates that the **Text** and **Image** components are not rebuilt.
1367
1368This is thanks to introduction of custom components, where state variables are directly changed through @Observed and @ObjectLink, instead of through **LazyForEach**. Decorate the **message** and **imgSrc** properties of the **StringData** type with [@Track](arkts-track.md) to further narrow down the render scope to the specified **Text** component.
1369
1370### Using Custom Components to Match Object Arrays in ForEach
1371
1372Frequently seen in applications, the combination of object arrays and [ForEach](arkts-rendering-control-foreach.md) requires special attentions. Inappropriate use may cause UI re-render issues.
1373
1374```typescript
1375@Observed
1376class StyleList extends Array<TextStyles> {
1377};
1378@Observed
1379class TextStyles {
1380  fontSize: number;
1381
1382  constructor(fontSize: number) {
1383    this.fontSize = fontSize;
1384  }
1385}
1386@Entry
1387@Component
1388struct Page {
1389  @State styleList: StyleList = new StyleList();
1390  aboutToAppear() {
1391    for (let i = 15; i < 50; i++)
1392    this.styleList.push(new TextStyles(i));
1393  }
1394  build() {
1395    Column() {
1396      Text("Font Size List")
1397        .fontSize(50)
1398        .onClick(() => {
1399          for (let i = 0; i < this.styleList.length; i++) {
1400            this.styleList[i].fontSize++;
1401          }
1402          console.log("change font size");
1403        })
1404      List() {
1405        ForEach(this.styleList, (item: TextStyles) => {
1406          ListItem() {
1407            Text("Hello World")
1408              .fontSize(item.fontSize)
1409          }
1410        })
1411      }
1412    }
1413  }
1414}
1415```
1416
1417Below you can see how the preceding code snippet works.
1418
1419![properly-use-state-management-to-develope-9](figures/properly-use-state-management-to-develope-9.gif)
1420
1421The items generated in **ForEach** are constants. This means that their value changes do not trigger UI re-renders. In this example, though an item is changed upon a click, as indicated by the "change font size" log, the UI is not updated as expected. To fix this issue, you need to use custom components with @ObjectLink.
1422
1423```typescript
1424@Observed
1425class StyleList extends Array<TextStyles> {
1426};
1427@Observed
1428class TextStyles {
1429  fontSize: number;
1430
1431  constructor(fontSize: number) {
1432    this.fontSize = fontSize;
1433  }
1434}
1435@Component
1436struct TextComponent {
1437  @ObjectLink textStyle: TextStyles;
1438  build() {
1439    Text("Hello World")
1440      .fontSize(this.textStyle.fontSize)
1441  }
1442}
1443@Entry
1444@Component
1445struct Page {
1446  @State styleList: StyleList = new StyleList();
1447  aboutToAppear() {
1448    for (let i = 15; i < 50; i++)
1449      this.styleList.push(new TextStyles(i));
1450  }
1451  build() {
1452    Column() {
1453      Text("Font Size List")
1454        .fontSize(50)
1455        .onClick(() => {
1456          for (let i = 0; i < this.styleList.length; i++) {
1457            this.styleList[i].fontSize++;
1458          }
1459          console.log("change font size");
1460        })
1461      List() {
1462        ForEach(this.styleList, (item: TextStyles) => {
1463          ListItem() {
1464            TextComponent({ textStyle: item})
1465          }
1466        })
1467      }
1468    }
1469  }
1470}
1471```
1472
1473Below you can see how the preceding code snippet works.
1474
1475![properly-use-state-management-to-develope-10](figures/properly-use-state-management-to-develope-10.gif)
1476
1477When @ObjectLink is used to accept the input item, the **textStyle** variable in the **TextComponent** component can be observed. For @ObjectLink, parameters are passed by reference. Therefore, when the value of **fontSize** in **styleList** is changed in the parent component, this update is properly observed and synced to the corresponding list item in **ForEach**, leading to UI re-rendering.
1478
1479This is a practical mode of using state management for UI re-rendering.
1480