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 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 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 373 374Click the **Move** button before optimization. The duration for updating dirty nodes is as follows. 375 376 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. 598 599Click the **Move** button after optimization. The duration for updating dirty nodes is as follows. 600 601 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 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 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 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 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 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 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