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