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 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 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')) // Use app.media.app_icon since API version 12. 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 CompA { 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')) // Use app.media.app_icon since API version 12. 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 CompA({ 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 366 367Click the **Move** button before optimization. The duration for updating dirty nodes is as follows. 368 369 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')) // Use app.media.app_icon since API version 12. 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 CompA { 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')) // Use app.media.app_icon since API version 12. 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 CompA({ 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. 590 591Click the **Move** button after optimization. The duration for updating dirty nodes is as follows. 592 593 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')) // Use app.media.app_icon since API version 12. 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 CompA { 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')) // Use app.media.app_icon since API version 12. 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 CompA({ 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 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 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 this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon'))); // Use app.media.app_icon since API version 12. 1189 } 1190 } 1191 1192 build() { 1193 List({ space: 3 }) { 1194 LazyForEach(this.data, (item: StringData, index: number) => { 1195 ListItem() { 1196 Column() { 1197 Text(item.message).fontSize(20) 1198 .onAppear(() => { 1199 console.info("text appear:" + item.message); 1200 }) 1201 Image(item.imgSrc) 1202 .width(100) 1203 .height(100) 1204 .onAppear(() => { 1205 console.info("image appear"); 1206 }) 1207 }.margin({ left: 10, right: 10 }) 1208 } 1209 .onClick(() => { 1210 item.message += '0'; 1211 this.data.reloadData(); 1212 }) 1213 }, (item: StringData, index: number) => JSON.stringify(item)) 1214 }.cachedCount(5) 1215 } 1216} 1217``` 1218 1219Below you can see how the preceding code snippet works. 1220 1221 1222 1223In 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. 1224 1225While 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. 1226 1227```typescript 1228class BasicDataSource implements IDataSource { 1229 private listeners: DataChangeListener[] = []; 1230 private originDataArray: StringData[] = []; 1231 1232 public totalCount(): number { 1233 return 0; 1234 } 1235 1236 public getData(index: number): StringData { 1237 return this.originDataArray[index]; 1238 } 1239 1240 registerDataChangeListener(listener: DataChangeListener): void { 1241 if (this.listeners.indexOf(listener) < 0) { 1242 console.info('add listener'); 1243 this.listeners.push(listener); 1244 } 1245 } 1246 1247 unregisterDataChangeListener(listener: DataChangeListener): void { 1248 const pos = this.listeners.indexOf(listener); 1249 if (pos >= 0) { 1250 console.info('remove listener'); 1251 this.listeners.splice(pos, 1); 1252 } 1253 } 1254 1255 notifyDataReload(): void { 1256 this.listeners.forEach(listener => { 1257 listener.onDataReloaded(); 1258 }) 1259 } 1260 1261 notifyDataAdd(index: number): void { 1262 this.listeners.forEach(listener => { 1263 listener.onDataAdd(index); 1264 }) 1265 } 1266 1267 notifyDataChange(index: number): void { 1268 this.listeners.forEach(listener => { 1269 listener.onDataChange(index); 1270 }) 1271 } 1272 1273 notifyDataDelete(index: number): void { 1274 this.listeners.forEach(listener => { 1275 listener.onDataDelete(index); 1276 }) 1277 } 1278 1279 notifyDataMove(from: number, to: number): void { 1280 this.listeners.forEach(listener => { 1281 listener.onDataMove(from, to); 1282 }) 1283 } 1284} 1285 1286class MyDataSource extends BasicDataSource { 1287 private dataArray: StringData[] = []; 1288 1289 public totalCount(): number { 1290 return this.dataArray.length; 1291 } 1292 1293 public getData(index: number): StringData { 1294 return this.dataArray[index]; 1295 } 1296 1297 public addData(index: number, data: StringData): void { 1298 this.dataArray.splice(index, 0, data); 1299 this.notifyDataAdd(index); 1300 } 1301 1302 public pushData(data: StringData): void { 1303 this.dataArray.push(data); 1304 this.notifyDataAdd(this.dataArray.length - 1); 1305 } 1306} 1307 1308@Observed 1309class StringData { 1310 @Track message: string; 1311 @Track imgSrc: Resource; 1312 constructor(message: string, imgSrc: Resource) { 1313 this.message = message; 1314 this.imgSrc = imgSrc; 1315 } 1316} 1317 1318@Entry 1319@Component 1320struct MyComponent { 1321 @State data: MyDataSource = new MyDataSource(); 1322 1323 aboutToAppear() { 1324 for (let i = 0; i <= 9; i++) { 1325 this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon'))); // Use app.media.app_icon since API version 12. 1326 } 1327 } 1328 1329 build() { 1330 List({ space: 3 }) { 1331 LazyForEach(this.data, (item: StringData, index: number) => { 1332 ListItem() { 1333 ChildComponent({data: item}) 1334 } 1335 .onClick(() => { 1336 item.message += '0'; 1337 }) 1338 }, (item: StringData, index: number) => index.toString()) 1339 }.cachedCount(5) 1340 } 1341} 1342 1343@Component 1344struct ChildComponent { 1345 @ObjectLink data: StringData 1346 build() { 1347 Column() { 1348 Text(this.data.message).fontSize(20) 1349 .onAppear(() => { 1350 console.info("text appear:" + this.data.message) 1351 }) 1352 Image(this.data.imgSrc) 1353 .width(100) 1354 .height(100) 1355 }.margin({ left: 10, right: 10 }) 1356 } 1357} 1358``` 1359 1360Below you can see how the preceding code snippet works. 1361 1362 1363 1364In 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. 1365 1366This 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. 1367 1368### Using Custom Components to Match Object Arrays in ForEach 1369 1370Frequently 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. 1371 1372```typescript 1373@Observed 1374class StyleList extends Array<TextStyle> { 1375}; 1376@Observed 1377class TextStyle { 1378 fontSize: number; 1379 1380 constructor(fontSize: number) { 1381 this.fontSize = fontSize; 1382 } 1383} 1384@Entry 1385@Component 1386struct Page { 1387 @State styleList: StyleList = new StyleList(); 1388 aboutToAppear() { 1389 for (let i = 15; i < 50; i++) 1390 this.styleList.push(new TextStyle(i)); 1391 } 1392 build() { 1393 Column() { 1394 Text("Font Size List") 1395 .fontSize(50) 1396 .onClick(() => { 1397 for (let i = 0; i < this.styleList.length; i++) { 1398 this.styleList[i].fontSize++; 1399 } 1400 console.log("change font size"); 1401 }) 1402 List() { 1403 ForEach(this.styleList, (item: TextStyle) => { 1404 ListItem() { 1405 Text("Hello World") 1406 .fontSize(item.fontSize) 1407 } 1408 }) 1409 } 1410 } 1411 } 1412} 1413``` 1414 1415Below you can see how the preceding code snippet works. 1416 1417 1418 1419The 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. 1420 1421```typescript 1422@Observed 1423class StyleList extends Array<TextStyle> { 1424}; 1425@Observed 1426class TextStyle { 1427 fontSize: number; 1428 1429 constructor(fontSize: number) { 1430 this.fontSize = fontSize; 1431 } 1432} 1433@Component 1434struct TextComponent { 1435 @ObjectLink textStyle: TextStyle; 1436 build() { 1437 Text("Hello World") 1438 .fontSize(this.textStyle.fontSize) 1439 } 1440} 1441@Entry 1442@Component 1443struct Page { 1444 @State styleList: StyleList = new StyleList(); 1445 aboutToAppear() { 1446 for (let i = 15; i < 50; i++) 1447 this.styleList.push(new TextStyle(i)); 1448 } 1449 build() { 1450 Column() { 1451 Text("Font Size List") 1452 .fontSize(50) 1453 .onClick(() => { 1454 for (let i = 0; i < this.styleList.length; i++) { 1455 this.styleList[i].fontSize++; 1456 } 1457 console.log("change font size"); 1458 }) 1459 List() { 1460 ForEach(this.styleList, (item: TextStyle) => { 1461 ListItem() { 1462 TextComponent({ textStyle: item}) 1463 } 1464 }) 1465 } 1466 } 1467 } 1468} 1469``` 1470 1471Below you can see how the preceding code snippet works. 1472 1473 1474 1475When @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. 1476 1477This is a practical mode of using state management for UI re-rendering. 1478 1479 1480 1481<!--no_check--> 1482