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