1# Best Practices for State Management 2 3 4This guide outlines best practices for state management in ArkUI applications. Read on to discover the common pitfalls in state management and how to avoid them, with carefully selected examples of recommended and not-recommended practices. 5 6 7## Basic Example 8 9The following example describes the initialization rules of the \@Prop, \@Link, and \@ObjectLink decorators. Before we dive in, a basic knowledge of these decorators is helpful. 10 11- \@Prop: An \@Prop decorated variable can be initialized from an \@State decorated variable of the parent component, a @State decorated attribute of the Object or class type in the parent component, or the item of an @State decorated array. 12 13- \@ObjectLink: The initialization rule is the same as that of \@Prop, but an \@ObjectLink decorated variable must be initialized from an instance of an \@Observed decorated class. 14 15- \@Link: The value type must be the same as that of \@State or any other data source. 16 17 18### Not Recommended 19 20 21 22```ts 23@Observed 24class ClassA { 25 public c: number = 0; 26 27 constructor(c: number) { 28 this.c = c; 29 } 30} 31 32@Component 33struct LinkChild { 34 @Link testNum: number; 35 36 build() { 37 Text(`LinkChild testNum ${this.testNum}`) 38 } 39} 40 41 42@Component 43struct PropChild2 { 44 @Prop testNum: ClassA = new ClassA(0); 45 46 build() { 47 Text(`PropChild2 testNum ${this.testNum.c}`) 48 .onClick(() => { 49 this.testNum.c += 1; 50 }) 51 } 52} 53 54@Component 55struct PropChild3 { 56 @Prop testNum: ClassA = new ClassA(0); 57 58 build() { 59 Text(`PropChild3 testNum ${this.testNum.c}`) 60 } 61} 62 63@Component 64struct ObjectLinkChild { 65 @ObjectLink testNum: ClassA; 66 67 build() { 68 Text(`ObjectLinkChild testNum ${this.testNum.c}`) 69 .onClick(() => { 70 // Issue 4: ObjectLink cannot be assigned a value. 71 this.testNum = new ClassA(47); 72 }) 73 } 74} 75 76@Entry 77@Component 78struct Parent { 79 @State testNum: ClassA[] = [new ClassA(1)]; 80 81 build() { 82 Column() { 83 Text(`Parent testNum ${this.testNum.c}`) 84 .onClick(() => { 85 this.testNum[0].c += 1; 86 }) 87 // Issue 1: The type of the @Link decorated variable is not the same as that of the data source @State. 88 LinkChild({ testNum: this.testNum.c }) 89 90 // Issue 2: The @Prop decorated variable is not initialized locally or initialized from the parent component. 91 PropChild2() 92 93 // Issue 3: PropChild3 does not change the value of @Prop testNum: ClassA. Therefore, @ObjectLink is a better choice here. 94 PropChild3({ testNum: this.testNum[0] }) 95 96 ObjectLinkChild({ testNum: this.testNum[0] }) 97 } 98 } 99} 100``` 101 102 103The preceding example contains several errors: 104 105 1061. \@Component LinkChild: The type of **\@Link testNum: number** and the initialization from the parent component **LinkChild ({testNum:this.testNum.c})** are incorrect. The data source of \@Link must be a decorated state variable. The \@Link decorated variables must be of the same type as the data source, for example, \@Link: T and \@State: T. Therefore, the value should be changed to **\@Link testNum: ClassA**, and the initialization from the parent component should be **LinkChild({testNum: $testNum})**. 107 1082. \@Component PropChild2: An \@Prop decorated variable can be initialized locally or from the parent component, but it must be initialized. **\@Prop testNum: ClassA** is not initialized locally, and therefore it must be initialized from the parent component: **PropChild1({testNum: this.testNum})**. 109 1103. \@Component PropChild3: The **\@Prop testNum: ClassA** value is not changed. Therefore, \@ObjectLink is a better choice here, because \@Prop involves a deep copy, which can result in an increase in overhead. 111 1124. Clicking ObjectLinkChild to assign a value to the \@ObjectLink decorated variable: **this.testNum = new ClassA(47);** is not allowed. For \@ObjectLink that implements two-way data synchronization, assigning a value is equivalent to updating the array item or class attribute in the parent component, which is not supported in TypeScript/JavaScript and will result in a runtime error. 113 1145. In a non-nested scenario, for example, where the variable declared in the parent is **\@State testNum: ClassA = new ClassA(1)**, **Class A** does not need to be decorated by \@Observed, since \@State is able to observe changes at the first layer. 115 116 117### Recommended 118 119 120 121```ts 122@Observed 123class ClassA { 124 public c: number = 0; 125 126 constructor(c: number) { 127 this.c = c; 128 } 129} 130 131@Component 132struct LinkChild { 133 @Link testNum: ClassA; 134 135 build() { 136 Text(`LinkChild testNum ${this.testNum?.c}`) 137 } 138} 139 140@Component 141struct PropChild1 { 142 @Prop testNum: ClassA = new ClassA(1); 143 144 build() { 145 Text(`PropChild1 testNum ${this.testNum?.c}`) 146 .onClick(() => { 147 this.testNum = new ClassA(48); 148 }) 149 } 150} 151 152@Component 153struct ObjectLinkChild { 154 @ObjectLink testNum: ClassA; 155 156 build() { 157 Text(`ObjectLinkChild testNum ${this.testNum.c}`) 158 // The @ObjectLink decorated variable can have the attribute updated. 159 .onClick(() => { 160 this.testNum.c += 1; 161 }) 162 } 163} 164 165@Entry 166@Component 167struct Parent { 168 @State testNum: ClassA[] = [new ClassA(1)]; 169 170 build() { 171 Column() { 172 Text(`Parent testNum ${this.testNum.c}`) 173 .onClick(() => { 174 this.testNum[0].c += 1; 175 }) 176 // The type of the @Link decorated variable must be the same as that of the data source @State. 177 LinkChild({ testNum: this.testNum[0] }) 178 179 // @Prop is initialized locally and therefore does not need to be initialized from the parent component. 180 PropChild1() 181 182 // When a child component does not need to be changed locally, @ObjectLink is preferred over @Prop, whose deep copy can result in an increase in overhead. 183 ObjectLinkChild({ testNum: this.testNum[0] }) 184 } 185 } 186} 187``` 188 189 190 191## UI Not Updating on Attribute Changes in Simple Nested Objects 192 193If you find your application UI not updating after an attribute in a nested object is changed, you may want to check the decorators in use. 194 195Each decorator has its scope of observable changes, and only those observed changes can cause the UI to update. The \@Observed decorator can observe the attribute changes of nested objects, while other decorators can observe only the changes at the second layer. 196 197 198### Not Recommended 199 200In the following example, some UI components are not updated. 201 202 203```ts 204class ClassA { 205 a: number; 206 207 constructor(a: number) { 208 this.a = a; 209 } 210 211 getA(): number { 212 return this.a; 213 } 214 215 setA(a: number): void { 216 this.a = a; 217 } 218} 219 220class ClassC { 221 c: number; 222 223 constructor(c: number) { 224 this.c = c; 225 } 226 227 getC(): number { 228 return this.c; 229 } 230 231 setC(c: number): void { 232 this.c = c; 233 } 234} 235 236class ClassB extends ClassA { 237 b: number = 47; 238 c: ClassC; 239 240 constructor(a: number, b: number, c: number) { 241 super(a); 242 this.b = b; 243 this.c = new ClassC(c); 244 } 245 246 getB(): number { 247 return this.b; 248 } 249 250 setB(b: number): void { 251 this.b = b; 252 } 253 254 getC(): number { 255 return this.c.getC(); 256 } 257 258 setC(c: number): void { 259 return this.c.setC(c); 260 } 261} 262 263 264@Entry 265@Component 266struct MyView { 267 @State b: ClassB = new ClassB(10, 20, 30); 268 269 build() { 270 Column({ space: 10 }) { 271 Text(`a: ${this.b.a}`) 272 Button("Change ClassA.a") 273 .onClick(() => { 274 this.b.a += 1; 275 }) 276 277 Text(`b: ${this.b.b}`) 278 Button("Change ClassB.b") 279 .onClick(() => { 280 this.b.b += 1; 281 }) 282 283 Text(`c: ${this.b.c.c}`) 284 Button("Change ClassB.ClassC.c") 285 .onClick(() => { 286 // The <Text> component is not updated when clicked. 287 this.b.c.c += 1; 288 }) 289 } 290 } 291} 292``` 293 294- The UI is not updated when the last **\<Text>** component Text('c: ${this.b.c.c}') is clicked. This is because, **\@State b: ClassB** can observe only the changes of the **this.b** attribute, such as **this.b.a**, **this.b.b**, and **this.b.c**, but cannot observe the attributes nested in the attribute, that is, **this.b.c.c** (attribute **c** is an attribute of the **ClassC** object nested in **b**). 295 296- To observe the attributes of nested object **ClassC**, you need to make the following changes: 297 - Construct a child component for separate rendering of the **ClassC** instance. Then, in this child component, you can use \@ObjectLink or \@Prop to decorate **c : ClassC**. In general cases, use \@ObjectLink, unless local changes to the **ClassC** object are required. 298 - The nested **ClassC** object must be decorated by \@Observed. When a **ClassC** object is created in **ClassB** (**ClassB(10, 20, 30)** in this example), it is wrapped in the ES6 proxy. When the **ClassC** attribute changes (this.b.c.c += 1), the \@ObjectLink decorated variable is notified of the change. 299 300 301### Recommended 302 303The following example uses \@Observed/\@ObjectLink to observe property changes for nested objects. 304 305 306```ts 307class ClassA { 308 a: number; 309 constructor(a: number) { 310 this.a = a; 311 } 312 getA() : number { 313 return this.a; } 314 setA( a: number ) : void { 315 this.a = a; } 316} 317 318@Observed 319class ClassC { 320 c: number; 321 constructor(c: number) { 322 this.c = c; 323 } 324 getC() : number { 325 return this.c; } 326 setC(c : number) : void { 327 this.c = c; } 328} 329 330class ClassB extends ClassA { 331 b: number = 47; 332 c: ClassC; 333 334 constructor(a: number, b: number, c: number) { 335 super(a); 336 this.b = b; 337 this.c = new ClassC(c); 338 } 339 340 getB() : number { 341 return this.b; } 342 setB(b : number) : void { 343 this.b = b; } 344 getC() : number { 345 return this.c.getC(); } 346 setC(c : number) : void { 347 return this.c.setC(c); } 348} 349 350@Component 351struct ViewClassC { 352 353 @ObjectLink c : ClassC; 354 build() { 355 Column({space:10}) { 356 Text(`c: ${this.c.getC()}`) 357 Button("Change C") 358 .onClick(() => { 359 this.c.setC(this.c.getC()+1); 360 }) 361 } 362 } 363} 364 365@Entry 366@Component 367struct MyView { 368 @State b : ClassB = new ClassB(10, 20, 30); 369 370 build() { 371 Column({space:10}) { 372 Text(`a: ${this.b.a}`) 373 Button("Change ClassA.a") 374 .onClick(() => { 375 this.b.a +=1; 376 }) 377 378 Text(`b: ${this.b.b}`) 379 Button("Change ClassB.b") 380 .onClick(() => { 381 this.b.b += 1; 382 }) 383 384 ViewClassC({c: this.b.c}) // Equivalent to Text(`c: ${this.b.c.c}`) 385 Button("Change ClassB.ClassC.c") 386 .onClick(() => { 387 this.b.c.c += 1; 388 }) 389 } 390 } 391} 392``` 393 394 395 396## UI Not Updating on Attribute Changes in Complex Nested Objects 397 398 399### Not Recommended 400 401The following example creates a child component with an \@ObjectLink decorated variable to render **ParentCounter** with nested attributes. **SubCounter** nested in **ParentCounter** is decorated with \@Observed. 402 403 404```ts 405let nextId = 1; 406@Observed 407class SubCounter { 408 counter: number; 409 constructor(c: number) { 410 this.counter = c; 411 } 412} 413@Observed 414class ParentCounter { 415 id: number; 416 counter: number; 417 subCounter: SubCounter; 418 incrCounter() { 419 this.counter++; 420 } 421 incrSubCounter(c: number) { 422 this.subCounter.counter += c; 423 } 424 setSubCounter(c: number): void { 425 this.subCounter.counter = c; 426 } 427 constructor(c: number) { 428 this.id = nextId++; 429 this.counter = c; 430 this.subCounter = new SubCounter(c); 431 } 432} 433@Component 434struct CounterComp { 435 @ObjectLink value: ParentCounter; 436 build() { 437 Column({ space: 10 }) { 438 Text(`${this.value.counter}`) 439 .fontSize(25) 440 .onClick(() => { 441 this.value.incrCounter(); 442 }) 443 Text(`${this.value.subCounter.counter}`) 444 .onClick(() => { 445 this.value.incrSubCounter(1); 446 }) 447 Divider().height(2) 448 } 449 } 450} 451@Entry 452@Component 453struct ParentComp { 454 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 455 build() { 456 Row() { 457 Column() { 458 CounterComp({ value: this.counter[0] }) 459 CounterComp({ value: this.counter[1] }) 460 CounterComp({ value: this.counter[2] }) 461 Divider().height(5) 462 ForEach(this.counter, 463 (item: ParentCounter) => { 464 CounterComp({ value: item }) 465 }, 466 (item: ParentCounter) => item.id.toString() 467 ) 468 Divider().height(5) 469 // First click event 470 Text('Parent: incr counter[0].counter') 471 .fontSize(20).height(50) 472 .onClick(() => { 473 this.counter[0].incrCounter(); 474 // The value increases by 10 each time the event is triggered. 475 this.counter[0].incrSubCounter(10); 476 }) 477 // Second click event 478 Text('Parent: set.counter to 10') 479 .fontSize(20).height(50) 480 .onClick(() => { 481 // The value cannot be set to 10, and the UI is not updated. 482 this.counter[0].setSubCounter(10); 483 }) 484 Text('Parent: reset entire counter') 485 .fontSize(20).height(50) 486 .onClick(() => { 487 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 488 }) 489 } 490 } 491 } 492} 493``` 494 495For the **onClick** event of **Text('Parent: incr counter[0].counter')**, **this.counter[0].incrSubCounter(10)** calls the **incrSubCounter** method to increase the **counter** value of **SubCounter** by 10. The UI is updated to reflect the change. 496 497However, when **this.counter[0].setSubCounter(10)** is called in **onClick** of **Text('Parent: set.counter to 10')**, the **counter** value of **SubCounter** cannot be reset to 10. 498 499**incrSubCounter** and **setSubCounter** are functions of the same **SubCounter**. The UI can be correctly updated when **incrSubCounter** is called for the first click event. However, the UI is not updated when **setSubCounter** is called for the second click event. Actually neither **incrSubCounter** nor **setSubCounter** can trigger an update of **Text('${this.value.subCounter.counter}')**. This is because **\@ObjectLink value: ParentCounter** can only observe the attributes of **ParentCounter**, and **this.value.subCounter.counter** is an attribute of **SubCounter** and therefore cannot be observed. 500 501However, when **this.counter[0].incrCounter()** is called for the first click event, it marks **\@ObjectLink value: ParentCounter** in the **CounterComp** component as changed. In this case, the update of **Text('${this.value.subCounter.counter}')** is triggered. If **this.counter[0].incrCounter()** is deleted from the first click event, the UI cannot be updated. 502 503 504### Recommended 505 506To solve the preceding problem, you can use the following method to directly observe the attributes in **SubCounter** so that the **this.counter[0].setSubCounter(10)** API works: 507 508 509```ts 510@ObjectLink value: ParentCounter = new ParentCounter(0); 511@ObjectLink subValue: SubCounter = new SubCounter(0); 512``` 513 514This approach enables \@ObjectLink to serve as a proxy for the attributes of the **ParentCounter** and **SubCounter** classes. In this way, the attribute changes of the two classes can be observed and trigger UI update. Even if **this.counter[0].incrCounter()** is deleted, the UI can be updated correctly. 515 516This method can be used to implement "two-layer" observation, that is, observation of external objects and internal nested objects. However, this method can only be used for the \@ObjectLink decorator and cannot be used for \@Prop (\@Prop passes objects through deep copy). For details, see the differences between @Prop and @ObjectLink. 517 518 519```ts 520let nextId = 1; 521@Observed 522class SubCounter { 523 counter: number; 524 constructor(c: number) { 525 this.counter = c; 526 } 527} 528@Observed 529class ParentCounter { 530 id: number; 531 counter: number; 532 subCounter: SubCounter; 533 incrCounter() { 534 this.counter++; 535 } 536 incrSubCounter(c: number) { 537 this.subCounter.counter += c; 538 } 539 setSubCounter(c: number): void { 540 this.subCounter.counter = c; 541 } 542 constructor(c: number) { 543 this.id = nextId++; 544 this.counter = c; 545 this.subCounter = new SubCounter(c); 546 } 547} 548@Component 549struct CounterComp { 550 @ObjectLink value: ParentCounter; 551 @ObjectLink subValue: SubCounter; 552 build() { 553 Column({ space: 10 }) { 554 Text(`${this.value.counter}`) 555 .fontSize(25) 556 .onClick(() => { 557 this.value.incrCounter(); 558 }) 559 Text(`${this.subValue.counter}`) 560 .onClick(() => { 561 this.subValue.counter += 1; 562 }) 563 Divider().height(2) 564 } 565 } 566} 567@Entry 568@Component 569struct ParentComp { 570 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 571 build() { 572 Row() { 573 Column() { 574 CounterComp({ value: this.counter[0], subValue: this.counter[0].subCounter }) 575 CounterComp({ value: this.counter[1], subValue: this.counter[1].subCounter }) 576 CounterComp({ value: this.counter[2], subValue: this.counter[2].subCounter }) 577 Divider().height(5) 578 ForEach(this.counter, 579 (item: ParentCounter) => { 580 CounterComp({ value: item, subValue: item.subCounter }) 581 }, 582 (item: ParentCounter) => item.id.toString() 583 ) 584 Divider().height(5) 585 Text('Parent: reset entire counter') 586 .fontSize(20).height(50) 587 .onClick(() => { 588 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 589 }) 590 Text('Parent: incr counter[0].counter') 591 .fontSize(20).height(50) 592 .onClick(() => { 593 this.counter[0].incrCounter(); 594 this.counter[0].incrSubCounter(10); 595 }) 596 Text('Parent: set.counter to 10') 597 .fontSize(20).height(50) 598 .onClick(() => { 599 this.counter[0].setSubCounter(10); 600 }) 601 } 602 } 603 } 604} 605``` 606 607 608## Differences Between \@Prop and \@ObjectLink 609 610In the following example, the \@ObjectLink decorated variable is a reference to the data source. That is, **this.value.subValue** and **this.subValue** are different references of the same object. Therefore, when the click handler of **CounterComp** is clicked, both **this.value.subCounter.counter** and **this.subValue.counter** change, and the corresponding component **Text (this.subValue.counter: ${this.subValue.counter})** is re-rendered. 611 612 613```ts 614let nextId = 1; 615 616@Observed 617class SubCounter { 618 counter: number; 619 constructor(c: number) { 620 this.counter = c; 621 } 622} 623 624@Observed 625class ParentCounter { 626 id: number; 627 counter: number; 628 subCounter: SubCounter; 629 incrCounter() { 630 this.counter++; 631 } 632 incrSubCounter(c: number) { 633 this.subCounter.counter += c; 634 } 635 setSubCounter(c: number): void { 636 this.subCounter.counter = c; 637 } 638 constructor(c: number) { 639 this.id = nextId++; 640 this.counter = c; 641 this.subCounter = new SubCounter(c); 642 } 643} 644 645@Component 646struct CounterComp { 647 @ObjectLink value: ParentCounter; 648 @ObjectLink subValue: SubCounter; 649 build() { 650 Column({ space: 10 }) { 651 Text(`this.subValue.counter: ${this.subValue.counter}`) 652 .fontSize(30) 653 Text(`this.value.counter: increase 7 `) 654 .fontSize(30) 655 .onClick(() => { 656 // click handler, Text(`this.subValue.counter: ${this.subValue.counter}`) will update 657 this.value.incrSubCounter(7); 658 }) 659 Divider().height(2) 660 } 661 } 662} 663 664@Entry 665@Component 666struct ParentComp { 667 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 668 build() { 669 Row() { 670 Column() { 671 CounterComp({ value: this.counter[0], subValue: this.counter[0].subCounter }) 672 CounterComp({ value: this.counter[1], subValue: this.counter[1].subCounter }) 673 CounterComp({ value: this.counter[2], subValue: this.counter[2].subCounter }) 674 Divider().height(5) 675 ForEach(this.counter, 676 (item: ParentCounter) => { 677 CounterComp({ value: item, subValue: item.subCounter }) 678 }, 679 (item: ParentCounter) => item.id.toString() 680 ) 681 Divider().height(5) 682 Text('Parent: reset entire counter') 683 .fontSize(20).height(50) 684 .onClick(() => { 685 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 686 }) 687 Text('Parent: incr counter[0].counter') 688 .fontSize(20).height(50) 689 .onClick(() => { 690 this.counter[0].incrCounter(); 691 this.counter[0].incrSubCounter(10); 692 }) 693 Text('Parent: set.counter to 10') 694 .fontSize(20).height(50) 695 .onClick(() => { 696 this.counter[0].setSubCounter(10); 697 }) 698 } 699 } 700 } 701} 702``` 703 704Below shows \@ObjectLink working in action. 705 706 707 708 709### Not Recommended 710 711If \@Prop is used instead of \@ObjectLink, then: When the first click handler is clicked, the UI is updated properly; However, when the second **onClick** event occurs, the first **Text** component of **CounterComp** is not re-rendered, because \@Prop makes a local copy of the variable. 712 713 **this.value.subCounter** and **this.subValue** are not the same object. Therefore, the change of **this.value.subCounter** does not change the copy object of **this.subValue**, and **Text(this.subValue.counter: ${this.subValue.counter})** is not re-rendered. 714 715```ts 716@Component 717struct CounterComp { 718 @Prop value: ParentCounter = new ParentCounter(0); 719 @Prop subValue: SubCounter = new SubCounter(0); 720 build() { 721 Column({ space: 10 }) { 722 Text(`this.subValue.counter: ${this.subValue.counter}`) 723 .fontSize(20) 724 .onClick(() => { 725 // 1st click handler 726 this.subValue.counter += 7; 727 }) 728 Text(`this.value.counter: increase 7 `) 729 .fontSize(20) 730 .onClick(() => { 731 // 2nd click handler 732 this.value.incrSubCounter(7); 733 }) 734 Divider().height(2) 735 } 736 } 737} 738``` 739 740Below shows \@Prop working in action. 741 742 743 744 745### Recommended 746 747Make only one copy of \@Prop value: ParentCounter from **ParentComp** to **CounterComp**. Do not make another copy of **SubCounter**. 748 749- Use only one **\@Prop counter: Counter** in the **CounterComp** component. 750 751- Add another child component **SubCounterComp** that contains **\@ObjectLink subCounter: SubCounter**. This \@ObjectLink ensures that changes to the **SubCounter** object attributes are observed and the UI is updated properly. 752 753- **\@ObjectLink subCounter: SubCounter** shares the same **SubCounter** object with **this.counter.subCounter** of **CounterComp**. 754 755 756```ts 757let nextId = 1; 758 759@Observed 760class SubCounter { 761 counter: number; 762 constructor(c: number) { 763 this.counter = c; 764 } 765} 766 767@Observed 768class ParentCounter { 769 id: number; 770 counter: number; 771 subCounter: SubCounter; 772 incrCounter() { 773 this.counter++; 774 } 775 incrSubCounter(c: number) { 776 this.subCounter.counter += c; 777 } 778 setSubCounter(c: number): void { 779 this.subCounter.counter = c; 780 } 781 constructor(c: number) { 782 this.id = nextId++; 783 this.counter = c; 784 this.subCounter = new SubCounter(c); 785 } 786} 787 788@Component 789struct SubCounterComp { 790 @ObjectLink subValue: SubCounter; 791 build() { 792 Text(`SubCounterComp: this.subValue.counter: ${this.subValue.counter}`) 793 .onClick(() => { 794 // 2nd click handler 795 this.subValue.incrSubCounter(7); 796 }) 797 } 798} 799@Component 800struct CounterComp { 801 @ObjectLink value: ParentCounter; 802 build() { 803 Column({ space: 10 }) { 804 Text(`this.value.incrCounter(): this.value.counter: ${this.value.counter}`) 805 .fontSize(20) 806 .onClick(() => { 807 // 1st click handler 808 this.value.incrCounter(); 809 }) 810 SubCounterComp({ subValue: this.value.subCounter }) 811 Text(`this.value.incrSubCounter()`) 812 .onClick(() => { 813 // 3rd click handler 814 this.value.incrSubCounter(77); 815 }) 816 Divider().height(2) 817 } 818 } 819} 820@Entry 821@Component 822struct ParentComp { 823 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 824 build() { 825 Row() { 826 Column() { 827 CounterComp({ value: this.counter[0] }) 828 CounterComp({ value: this.counter[1] }) 829 CounterComp({ value: this.counter[2] }) 830 Divider().height(5) 831 ForEach(this.counter, 832 (item: ParentCounter) => { 833 CounterComp({ value: item }) 834 }, 835 (item: ParentCounter) => item.id.toString() 836 ) 837 Divider().height(5) 838 Text('Parent: reset entire counter') 839 .fontSize(20).height(50) 840 .onClick(() => { 841 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 842 }) 843 Text('Parent: incr counter[0].counter') 844 .fontSize(20).height(50) 845 .onClick(() => { 846 this.counter[0].incrCounter(); 847 this.counter[0].incrSubCounter(10); 848 }) 849 Text('Parent: set.counter to 10') 850 .fontSize(20).height(50) 851 .onClick(() => { 852 this.counter[0].setSubCounter(10); 853 }) 854 } 855 } 856 } 857} 858``` 859 860 861Below shows the copy relationship. 862 863 864 865 866 867## Application Not Allowed to Change State Variables During Rendering 868 869Before learning this example, keep in mind that, in ArkUI state management, UI update is driven by state. 870 871 872 873Therefore, state variables cannot be directly changed in the **build()** or \@Builder decorated method of a custom component, as this may cause loop rendering risks. The following uses the **build()** method as an example. 874 875 876### Not Recommended 877 878In the following example, **Text('${this.count++}')** directly changes the state variable in the **build()** method. 879 880 881```ts 882@Entry 883@Component 884struct CompA { 885 @State col1: Color = Color.Yellow; 886 @State col2: Color = Color.Green; 887 @State count: number = 1; 888 build() { 889 Column() { 890 // Do not directly change the value of count in the Text component. 891 Text(`${this.count++}`) 892 .width(50) 893 .height(50) 894 .fontColor(this.col1) 895 .onClick(() => { 896 this.col2 = Color.Red; 897 }) 898 Button("change col1").onClick(() =>{ 899 this.col1 = Color.Pink; 900 }) 901 } 902 .backgroundColor(this.col2) 903 } 904} 905``` 906 907In ArkUI, the full and minimum updates of **Text('${this.count++}')** impose different effects: 908 909- Full update: ArkUI may fall into an infinite re-rendering loop because each rendering of the **Text** component changes the application state and causes the next rendering to start. When **this.col2** is changed, the entire **build** function is executed. As a result, the text bound to **Text(${this.count++})** is also changed. Each time **Text(${this.count++})** is rendered, the **this.count** state variable is updated, and a new round of **build** execution follows, resulting in an infinite loop. 910 911- Minimum update: When **this.col2** is changed, only the **Column** component is updated, and the **Text** component remains unchanged. When **this.col1** is changed, the entire **Text** component is updated and all of its attribute functions are executed. As a result, the value of **${this.count++}** in the **Text** component is changed. Currently, the UI is updated by component. If an attribute of a component changes, the entire component is updated. Therefore, the overall update link is as follows: **this.col2** = **Color.Red** - > **Text** component update - > **this.count++** - > **Text** component update. 912 913 914### Recommended 915 916When possible, perform the count++ operation in the event handler. 917 918 919```ts 920@Entry 921@Component 922struct CompA { 923 @State col1: Color = Color.Yellow; 924 @State col2: Color = Color.Green; 925 @State count: number = 1; 926 build() { 927 Column() { 928 Text(`${this.count}`) 929 .width(50) 930 .height(50) 931 .backgroundColor(this.col1) 932 .onClick(() => { 933 this.count++; 934 }) 935 } 936 .backgroundColor(this.col2) 937 } 938} 939``` 940 941The behavior of changing the application state in the **build** function may be more covert than that in the preceding example. The following are some examples: 942 943- Changing the state variable within the \@Builder, \@Extend, or \@Styles decorated method 944 945- Changing the application state variable in the function called during parameter calculation, for example, **Text('${this.calcLabel()}')** 946 947- Modifying the current array: In the following code snippet, **sort()** changes the array **this.arr**, and the subsequent **filter** method returns a new array. 948 949 950```ts 951@State arr : Array<...> = [ ... ]; 952ForEach(this.arr.sort().filter(...), 953 item => { 954 ... 955}) 956``` 957 958In the correct invoking sequence, **filter**, which returns a new array, is called before **sort()**. In this way, the **sort()** method does not change the array **this.arr**. 959 960 961```ts 962ForEach(this.arr.filter(...).sort(), 963 item => { 964 ... 965}) 966``` 967 968 969## Forcibly Updating Data Through State Variables 970 971 972### Not Recommended 973 974 975```ts 976@Entry 977@Component 978struct CompA { 979 @State needsUpdate: boolean = true; 980 realState1: Array<number> = [4, 1, 3, 2]; // No state variable decorator is used. 981 realState2: Color = Color.Yellow; 982 983 updateUI1(param: Array<number>): Array<number> { 984 const triggerAGet = this.needsUpdate; 985 return param; 986 } 987 updateUI2(param: Color): Color { 988 const triggerAGet = this.needsUpdate; 989 return param; 990 } 991 build() { 992 Column({ space: 20 }) { 993 ForEach(this.updateUI1(this.realState1), 994 (item: Array<number>) => { 995 Text(`${item}`) 996 }) 997 Text("add item") 998 .onClick(() => { 999 // Changing realState1 does not trigger UI update. 1000 this.realState1.push(this.realState1[this.realState1.length-1] + 1); 1001 1002 // Trigger the UI update. 1003 this.needsUpdate = !this.needsUpdate; 1004 }) 1005 Text("chg color") 1006 .onClick(() => { 1007 // Changing realState2 does not trigger UI update. 1008 this.realState2 = this.realState2 == Color.Yellow ? Color.Red : Color.Yellow; 1009 1010 // Trigger the UI update. 1011 this.needsUpdate = !this.needsUpdate; 1012 }) 1013 }.backgroundColor(this.updateUI2(this.realState2)) 1014 .width(200).height(500) 1015 } 1016} 1017``` 1018 1019The preceding example has the following pitfalls: 1020 1021- The application wants to control the UI update logic, but in ArkUI, the UI update logic should be implemented by the framework detecting changes to the application state variables. 1022 1023- **this.needsUpdate** is a custom state variable that should be applied only to the UI component to which it is bound. Because **this.realState1** and **this.realState2** are regular variables (not decorated), their changes do not trigger UI update. 1024 1025- However, in this application, the user attempts to update the two regular variables through **this.needsUpdate**. This approach is nonviable and may result in poor update performance: The change of **this.needsUpdate** will cause ForEach to update, even if the original intent is to update only the background color. 1026 1027 1028### Recommended 1029 1030To address this issue, decorate the **realState1** and **realState2** variables with \@State. Then, the variable **needsUpdate** is no longer required. 1031 1032 1033```ts 1034@Entry 1035@Component 1036struct CompA { 1037 @State realState1: Array<number> = [4, 1, 3, 2]; 1038 @State realState2: Color = Color.Yellow; 1039 build() { 1040 Column({ space: 20 }) { 1041 ForEach(this.realState1, 1042 (item: Array<number>) => { 1043 Text(`${item}`) 1044 }) 1045 Text("add item") 1046 .onClick(() => { 1047 // Changing realState1 triggers UI update. 1048 this.realState1.push(this.realState1[this.realState1.length-1] + 1); 1049 }) 1050 Text("chg color") 1051 .onClick(() => { 1052 // Changing realState2 triggers UI update. 1053 this.realState2 = this.realState2 == Color.Yellow ? Color.Red : Color.Yellow; 1054 }) 1055 }.backgroundColor(this.realState2) 1056 .width(200).height(500) 1057 } 1058} 1059``` 1060 1061## Component Reuse 1062 1063If @Prop is nested with too many layers of data, garbage collection and increased memory usage caused by deep copy will follow, resulting in performance issues. In the following examples, using @Reusable to pass data from the parent component to the child component is recommended, and nesting @Prop with more than five layers of data is not recommended. 1064 1065### Not Recommended 1066 1067```ts 1068// The following is the data structure of a nested class object. 1069@Observed 1070class ClassA { 1071 public title: string; 1072 1073 constructor(title: string) { 1074 this.title = title; 1075 } 1076} 1077 1078@Observed 1079class ClassB { 1080 public name: string; 1081 public a: ClassA; 1082 1083 constructor(name: string, a: ClassA) { 1084 this.name = name; 1085 this.a = a; 1086 } 1087} 1088 1089@Observed 1090class ClassC { 1091 public name: string; 1092 public b: ClassB; 1093 1094 constructor(name: string, b: ClassB) { 1095 this.name = name; 1096 this.b = b; 1097 } 1098} 1099 1100@Observed 1101class ClassD { 1102 public name: string; 1103 public c: ClassC; 1104 1105 constructor(name: string, c: ClassC) { 1106 this.name = name; 1107 this.c = c; 1108 } 1109} 1110 1111@Observed 1112class ClassE { 1113 public name: string; 1114 public d: ClassD; 1115 1116 constructor(name: string, d: ClassD) { 1117 this.name = name; 1118 this.d = d; 1119 } 1120} 1121 1122``` 1123 1124The following component hierarchy presents a data structure of nested @Prop. 1125 1126```ts 1127@Entry 1128@Component 1129struct Parent { 1130 @State vote: ClassE = new ClassE('Hi', new ClassD('OpenHarmony', new ClassC('Hello', new ClassB('World', new ClassA('Peace'))))) 1131 1132 build() { 1133 Column() { 1134 Button('change') 1135 .onClick(() => { 1136 this.vote.name = "Hello" 1137 }) 1138 Child({ voteOne: this.vote }) 1139 } 1140 } 1141} 1142 1143@Component 1144struct Child { 1145 @ObjectLink voteOne: ClassE 1146 build() { 1147 Column() { 1148 Text(this.voteOne.name).fontSize(24).fontColor(Color.Red).margin(50) 1149 .onClick(() => { 1150 console.log('this.voteOne.name:' + this.voteOne.name); 1151 this.voteOne.name = 'Bye' 1152 }) 1153 ChildOne({voteTwo:this.voteOne.d}) 1154 } 1155 } 1156} 1157 1158@Component 1159struct ChildOne { 1160 @ObjectLink voteTwo: ClassD 1161 build() { 1162 Column() { 1163 Text(this.voteTwo.name).fontSize(24).fontColor(Color.Red).margin(50) 1164 .onClick(() => { 1165 console.log('this.voteTwo.name:' + this.voteTwo.name); 1166 this.voteTwo.name = 'Bye Bye' 1167 }) 1168 ChildTwo({voteThree:this.voteTwo.c}) 1169 } 1170 } 1171} 1172 1173@Component 1174struct ChildTwo { 1175 @ObjectLink voteThree: ClassC 1176 build() { 1177 Column() { 1178 Text(this.voteThree.name).fontSize(24).fontColor(Color.Red).margin(50) 1179 .onClick(() => { 1180 console.log('this.voteThree.name:' + this.voteThree.name); 1181 this.voteThree.name = 'Bye Bye Bye' 1182 }) 1183 ChildThree({voteFour:this.voteThree.b}) 1184 } 1185 } 1186} 1187 1188@Component 1189struct ChildThree { 1190 @ObjectLink voteFour: ClassB 1191 build() { 1192 Column() { 1193 Text(this.voteFour.name).fontSize(24).fontColor(Color.Red).margin(50) 1194 .onClick(() => { 1195 console.log('this.voteFour.name:' + this.voteFour.name); 1196 this.voteFour.name = 'Bye Bye Bye Bye' 1197 }) 1198 ChildFour({voteFive:this.voteFour.a}) 1199 } 1200 } 1201} 1202 1203@Component 1204struct ChildFour { 1205 @ObjectLink voteFive: ClassA 1206 build() { 1207 Column() { 1208 Text(this.voteFive.title).fontSize(24).fontColor(Color.Red).margin(50) 1209 .onClick(() => { 1210 console.log('this.voteFive.title:' + this.voteFive.title); 1211 this.voteFive.title = 'Bye Bye Bye Bye Bye' 1212 }) 1213 } 1214 } 1215} 1216``` 1217 1218### Recommended 1219 1220In component reuse scenarios, if you do not want to synchronize the data of a child component to the parent component, consider using **aboutToReuse** in @Reusable to pass data from the parent component to the child component. 1221 1222```ts 1223// The following is the data structure of a nested class object. 1224@Observed 1225class ClassA { 1226 public title: string; 1227 1228 constructor(title: string) { 1229 this.title = title; 1230 } 1231} 1232 1233@Observed 1234class ClassB { 1235 public name: string; 1236 public a: ClassA; 1237 1238 constructor(name: string, a: ClassA) { 1239 this.name = name; 1240 this.a = a; 1241 } 1242} 1243 1244@Observed 1245class ClassC { 1246 public name: string; 1247 public b: ClassB; 1248 1249 constructor(name: string, b: ClassB) { 1250 this.name = name; 1251 this.b = b; 1252 } 1253} 1254 1255@Observed 1256class ClassD { 1257 public name: string; 1258 public c: ClassC; 1259 1260 constructor(name: string, c: ClassC) { 1261 this.name = name; 1262 this.c = c; 1263 } 1264} 1265 1266@Observed 1267class ClassE { 1268 public name: string; 1269 public d: ClassD; 1270 1271 constructor(name: string, d: ClassD) { 1272 this.name = name; 1273 this.d = d; 1274 } 1275} 1276 1277``` 1278 1279The following component hierarchy presents a data structure of @Reusable. 1280 1281```ts 1282// The following is the data structure of a nested class object. 1283@Observed 1284class ClassA { 1285 public title: string; 1286 1287 constructor(title: string) { 1288 this.title = title; 1289 } 1290} 1291 1292@Observed 1293class ClassB { 1294 public name: string; 1295 public a: ClassA; 1296 1297 constructor(name: string, a: ClassA) { 1298 this.name = name; 1299 this.a = a; 1300 } 1301} 1302 1303@Observed 1304class ClassC { 1305 public name: string; 1306 public b: ClassB; 1307 1308 constructor(name: string, b: ClassB) { 1309 this.name = name; 1310 this.b = b; 1311 } 1312} 1313 1314@Observed 1315class ClassD { 1316 public name: string; 1317 public c: ClassC; 1318 1319 constructor(name: string, c: ClassC) { 1320 this.name = name; 1321 this.c = c; 1322 } 1323} 1324 1325@Observed 1326class ClassE { 1327 public name: string; 1328 public d: ClassD; 1329 1330 constructor(name: string, d: ClassD) { 1331 this.name = name; 1332 this.d = d; 1333 } 1334} 1335@Entry 1336@Component 1337struct Parent { 1338 @State vote: ClassE = new ClassE('Hi', new ClassD('OpenHarmony', new ClassC('Hello', new ClassB('World', new ClassA('Peace'))))) 1339 1340 build() { 1341 Column() { 1342 Button('change') 1343 .onClick(() => { 1344 this.vote.name = "Hello" 1345 }) 1346 .reuseId(Child.name) 1347 Child({voteOne: this.vote}) 1348 } 1349 } 1350} 1351 1352@Reusable 1353@Component 1354struct Child { 1355 @State voteOne: ClassE = new ClassE('voteOne', new ClassD('OpenHarmony', new ClassC('Hello', new ClassB('World', new ClassA('Peace'))))) 1356 1357 aboutToReuse(params: ClassE) { 1358 this.voteOne = params 1359 } 1360 build() { 1361 Column() { 1362 Text(this.voteOne.name).fontSize(24).fontColor(Color.Red).margin(50) 1363 .onClick(() => { 1364 console.error('this.voteOne.name:' + this.voteOne.name); 1365 this.voteOne.name = 'Bye' 1366 }) 1367 .reuseId(ChildOne.name) 1368 ChildOne({voteTwo: this.voteOne.d}) 1369 } 1370 } 1371} 1372 1373@Reusable 1374@Component 1375struct ChildOne { 1376 @State voteTwo: ClassD = new ClassD('voteTwo', new ClassC('Hello', new ClassB('World', new ClassA('Peace')))) 1377 aboutToReuse(params: ClassD){ 1378 this.voteTwo = params 1379 } 1380 build() { 1381 Column() { 1382 Text(this.voteTwo.name).fontSize(24).fontColor(Color.Red).margin(50) 1383 .onClick(() => { 1384 console.error('this.voteTwo.name:' + this.voteTwo.name); 1385 this.voteTwo.name = 'Bye Bye' 1386 }) 1387 .reuseId(ChildTwo.name) 1388 ChildTwo({voteThree: this.voteTwo.c}) 1389 } 1390 } 1391} 1392 1393@Reusable 1394@Component 1395struct ChildTwo { 1396 @State voteThree: ClassC = new ClassC('voteThree', new ClassB('World', new ClassA('Peace'))) 1397 aboutToReuse(params: ClassC){ 1398 this.voteThree = params 1399 1400 } 1401 build() { 1402 Column() { 1403 Text(this.voteThree.name).fontSize(24).fontColor(Color.Red).margin(50) 1404 .onClick(() => { 1405 console.log('this.voteThree.name:' + this.voteThree.name); 1406 this.voteThree.name = 'Bye Bye Bye' 1407 }) 1408 .reuseId(ChildThree.name) 1409 ChildThree({voteFour: this.voteThree.b}) 1410 } 1411 } 1412} 1413 1414@Reusable 1415@Component 1416struct ChildThree { 1417 @State voteFour: ClassB = new ClassB('voteFour', new ClassA('Peace')) 1418 aboutToReuse(params: ClassB){ 1419 this.voteFour = params 1420 1421 } 1422 build() { 1423 Column() { 1424 Text(this.voteFour.name).fontSize(24).fontColor(Color.Red).margin(50) 1425 .onClick(() => { 1426 console.log('this.voteFour.name:' + this.voteFour.name); 1427 this.voteFour.name = 'Bye Bye Bye Bye' 1428 }) 1429 .reuseId(ChildFour.name) 1430 ChildFour({voteFive: this.voteFour.a}) 1431 } 1432 } 1433} 1434 1435@Reusable 1436@Component 1437struct ChildFour { 1438 @State voteFive: ClassA = new ClassA('voteFive') 1439 aboutToReuse(params: ClassA){ 1440 this.voteFive = params 1441 1442 } 1443 build() { 1444 Column() { 1445 Text(this.voteFive.title).fontSize(24).fontColor(Color.Red).margin(50) 1446 .onClick(() => { 1447 console.log('this.voteFive.title:' + this.voteFive.title); 1448 this.voteFive.title = 'Bye Bye Bye Bye Bye' 1449 }) 1450 } 1451 } 1452} 1453``` 1454