• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Freezing a Custom Component
2
3Freezing a custom component is designed to optimize the performance of complex UI pages, especially for scenarios where multiple page stacks, long lists, or grid layouts are involved. In these cases, when the state variable is bound to multiple UI components, the change of the state variables may trigger the re-render of a large number of UI components, resulting in frame freezing and response delay. To improve the UI re-render performance, you can try to use the custom component freezing function.
4
5Principles of freezing a component are as follows:
61. Setting the **freezeWhenInactive** attribute to activate the component freezing mechanism.
72. After this function is enabled, the system re-renders only the activated custom components. In this way, the UI framework can narrow down the re-render scope to the (activated) custom components that are visible to users, improving the re-render efficiency in complex UI scenarios.
83. When an inactive custom component turns into the active state, the state management framework performs necessary re-render operations on the custom component to ensure that the UI is correctly displayed.
9
10In short, component freezing aims to optimize UI re-render performance on complex UIs. When there are multiple invisible custom components, such as multiple page stacks, long lists, or grids, you can freeze the components to re-render visible custom components as required, and the re-render of the invisible custom components is delayed until they become visible.
11
12Note that the active or inactive state of a component is not equivalent to its visibility. Component freezing applies only to the following scenarios:
13
141. Page routing: The current top page of the navigation stack is in the active state, and the non-top invisible page is in the inactive state.
152. TabContent: Only the custom component in the currently displayed TabContent is in the active state.
163. LazyForEach: Only the custom component in the currently displayed LazyForEach is in the active state, and the component of the cache node is in the inactive state.
174. Navigation: Only the custom component in the currently displayed NavDestination is in the active state.
185. Component reuse: The component that enters the reuse pool is in the inactive state, and the node attached from the reuse pool is in the active state.
196. Mixed use: For example, if **LazyForEach** is used under **TabContent**, all nodes in **LazyForEach** of API version 17 or earlier are set to the active state since when switching tabs. Since API version 18, only the on-screen nodes of **LazyForEach** are set to the active state, and other nodes are set to the inactive state.
20
21Before reading this topic, you are advised to read [Creating a Custom Component](./arkts-create-custom-components.md) to learn about the basic syntax.
22
23> **NOTE**
24>
25> Custom component freezing is supported since API version 11.
26>
27> Mixed use of custom component freezing is supported since API version 18.
28
29## Use Scenarios
30
31### Page Routing
32
33> **NOTE**
34>
35> This example uses router for page redirection but you are advised to use the **Navigation** component instead, because **Navigation** provides more functions and more flexible customization capabilities. For details, see the use cases of [Navigation](#navigation).
36
37When page 1 calls the **router.pushUrl** API to jump to page 2, page 1 is hidden and invisible. In this case, if the state variable on page 1 is updated, page 1 is not re-rendered.
38For details, see the following.
39
40![freezeInPage](./figures/freezeInPage.png)
41
42Page 1
43
44```ts
45import { router } from '@kit.ArkUI';
46
47@Entry
48@Component({ freezeWhenInactive: true })
49struct Page1 {
50  @StorageLink('PropA') @Watch("first") storageLink: number = 47;
51
52  first() {
53    console.info("first page " + `${this.storageLink}`)
54  }
55
56  build() {
57    Column() {
58      Text(`From first Page ${this.storageLink}`).fontSize(50)
59      Button('first page storageLink + 1').fontSize(30)
60        .onClick(() => {
61          this.storageLink += 1
62        })
63      Button('go to next page').fontSize(30)
64        .onClick(() => {
65          router.pushUrl({ url: 'pages/Page2' })
66        })
67    }
68  }
69}
70```
71
72Page 2
73
74```ts
75import { router } from '@kit.ArkUI';
76
77@Entry
78@Component({ freezeWhenInactive: true })
79struct Page2 {
80  @StorageLink('PropA') @Watch("second") storageLink2: number = 1;
81
82  second() {
83    console.info("second page: " + `${this.storageLink2}`)
84  }
85
86  build() {
87    Column() {
88
89      Text(`second Page ${this.storageLink2}`).fontSize(50)
90      Button('Change Divider.strokeWidth')
91        .onClick(() => {
92          router.back()
93        })
94
95      Button('second page storageLink2 + 2').fontSize(30)
96        .onClick(() => {
97          this.storageLink2 += 2
98        })
99
100    }
101  }
102}
103```
104
105In the preceding example:
106
1071. When the button **first page storageLink + 1** on page 1 is clicked, the **storageLink** state variable is updated, and the @Watch decorated **first** method is called.
108
1092. Through **router.pushUrl({url:'pages/second'})**, page 2 is displayed, and page 1 is hidden with its state changing from active to inactive.
110
1113. When the button **this.storageLink2 += 2** on page 2 is clicked, only the @Watch decorated **second** method of page 2 is called, because page 1 has been frozen when inactive.
112
1134. When the **back** button is clicked, page 2 is destroyed, and page 1 changes from inactive to active. At this time, if the state variable of page 1 is updated, the @Watch decorated **first** method of page 1 is called again.
114
115
116### TabContent
117
118- You can freeze invisible **TabContent** components in the **Tabs** container so that they do not trigger UI re-rendering.
119
120- During initial rendering, only the **TabContent** component that is being displayed is created. All **TabContent** components are created only after all of them have been switched to.
121
122For details, see the following.
123![freezeWithTab](./figures/freezewithTabs.png)
124
125```ts
126@Entry
127@Component
128struct TabContentTest {
129  @State @Watch("onMessageUpdated") message: number = 0;
130  private data: number[] = [0, 1]
131
132  onMessageUpdated() {
133    console.info(`TabContent message callback func ${this.message}`)
134  }
135
136  build() {
137    Row() {
138      Column() {
139        Button('change message').onClick(() => {
140          this.message++
141        })
142
143        Tabs() {
144          ForEach(this.data, (item: number) => {
145            TabContent() {
146              FreezeChild({ message: this.message, index: item })
147            }.tabBar(`tab${item}`)
148          }, (item: number) => item.toString())
149        }
150      }
151      .width('100%')
152    }
153    .height('100%')
154  }
155}
156
157@Component({ freezeWhenInactive: true })
158struct FreezeChild {
159  @Link @Watch("onMessageUpdated") message: number
160  private index: number = 0
161
162  onMessageUpdated() {
163    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
164  }
165
166  build() {
167    Text("message" + `${this.message}, index: ${this.index}`)
168      .fontSize(50)
169      .fontWeight(FontWeight.Bold)
170  }
171}
172```
173
174In the preceding example:
175
1761. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called.
177
1782. When you click **two** to switch to another **TabContent** component, it switches from inactive to active, and the corresponding @Watch decorated **onMessageUpdated** method is called.
179
1803. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called.
181
182![TabContent.gif](figures/TabContent.gif)
183
184
185### LazyForEach
186
187- You can freeze custom components cached in **LazyForEach** so that they do not trigger UI re-rendering.
188
189```ts
190// Basic implementation of IDataSource used to listening for data.
191class BasicDataSource implements IDataSource {
192  private listeners: DataChangeListener[] = [];
193  private originDataArray: string[] = [];
194
195  public totalCount(): number {
196    return 0;
197  }
198
199  public getData(index: number): string {
200    return this.originDataArray[index];
201  }
202
203  // This method is called by the framework to add a listener to the LazyForEach data source.
204  registerDataChangeListener(listener: DataChangeListener): void {
205    if (this.listeners.indexOf(listener) < 0) {
206      console.info('add listener');
207      this.listeners.push(listener);
208    }
209  }
210
211  // This method is called by the framework to remove the listener from the LazyForEach data source.
212  unregisterDataChangeListener(listener: DataChangeListener): void {
213    const pos = this.listeners.indexOf(listener);
214    if (pos >= 0) {
215      console.info('remove listener');
216      this.listeners.splice(pos, 1);
217    }
218  }
219
220  // Notify LazyForEach that all child components need to be reloaded.
221  notifyDataReload(): void {
222    this.listeners.forEach(listener => {
223      listener.onDataReloaded();
224    })
225  }
226
227  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
228  notifyDataAdd(index: number): void {
229    this.listeners.forEach(listener => {
230      listener.onDataAdd(index);
231    })
232  }
233
234  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
235  notifyDataChange(index: number): void {
236    this.listeners.forEach(listener => {
237      listener.onDataChange(index);
238    })
239  }
240
241  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
242  notifyDataDelete(index: number): void {
243    this.listeners.forEach(listener => {
244      listener.onDataDelete(index);
245    })
246  }
247}
248
249class MyDataSource extends BasicDataSource {
250  private dataArray: string[] = [];
251
252  public totalCount(): number {
253    return this.dataArray.length;
254  }
255
256  public getData(index: number): string {
257    return this.dataArray[index];
258  }
259
260  public addData(index: number, data: string): void {
261    this.dataArray.splice(index, 0, data);
262    this.notifyDataAdd(index);
263  }
264
265  public pushData(data: string): void {
266    this.dataArray.push(data);
267    this.notifyDataAdd(this.dataArray.length - 1);
268  }
269}
270
271@Entry
272@Component
273struct LforEachTest {
274  private data: MyDataSource = new MyDataSource();
275  @State @Watch("onMessageUpdated") message: number = 0;
276
277  onMessageUpdated() {
278    console.info(`LazyforEach message callback func ${this.message}`)
279  }
280
281  aboutToAppear() {
282    for (let i = 0; i <= 20; i++) {
283      this.data.pushData(`Hello ${i}`)
284    }
285  }
286
287  build() {
288    Column() {
289      Button('change message').onClick(() => {
290        this.message++
291      })
292      List({ space: 3 }) {
293        LazyForEach(this.data, (item: string) => {
294          ListItem() {
295            FreezeChild({ message: this.message, index: item })
296          }
297        }, (item: string) => item)
298      }.cachedCount(5).height(500)
299    }
300
301  }
302}
303
304@Component({ freezeWhenInactive: true })
305struct FreezeChild {
306  @Link @Watch("onMessageUpdated") message: number;
307  private index: string = "";
308
309  aboutToAppear() {
310    console.info(`FreezeChild aboutToAppear index: ${this.index}`)
311  }
312
313  onMessageUpdated() {
314    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
315  }
316
317  build() {
318    Text("message" + `${this.message}, index: ${this.index}`)
319      .width('90%')
320      .height(160)
321      .backgroundColor(0xAFEEEE)
322      .textAlign(TextAlign.Center)
323      .fontSize(30)
324      .fontWeight(FontWeight.Bold)
325  }
326}
327```
328
329In the preceding example:
330
3311. When **change message** is clicked, the value of **message** changes, the @Watch decorated **onMessageUpdated** method of the list items being displayed is called, and that of the cached list items is not called. (If the component is not frozen, the @Watch decorated **onMessageUpdated** method of both list items that are being displayed and cached list items is called.)
332
3332. When a list item moves from outside the list content area into the list content area, it switches from inactive to active, and the corresponding @Watch decorated **onMessageUpdated** method is called.
334
3353. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **onMessageUpdated** method of the list items being displayed is called.
336
337![FrezzeLazyforEach.gif](figures/FrezzeLazyforEach.gif)
338
339### Navigation
340
341- When the navigation destination page is invisible, its child custom components are set to the inactive state and will not be re-rendered. When return to this page, its child custom components are restored to the active state and the @Watch callback is triggered to re-render the page.
342
343- In the following example, **NavigationContentMsgStack** is set to the inactive state, which does not respond to the change of the state variables, and does not trigger component re-rendering.
344
345```ts
346@Entry
347@Component
348struct MyNavigationTestStack {
349  @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
350  @State @Watch("info") message: number = 0;
351  @State logNumber: number = 0;
352
353  info() {
354    console.info(`freeze-test MyNavigation message callback ${this.message}`);
355  }
356
357  @Builder
358  PageMap(name: string) {
359    if (name === 'pageOne') {
360      pageOneStack({ message: this.message, logNumber: this.logNumber })
361    } else if (name === 'pageTwo') {
362      pageTwoStack({ message: this.message, logNumber: this.logNumber })
363    } else if (name === 'pageThree') {
364      pageThreeStack({ message: this.message, logNumber: this.logNumber })
365    }
366  }
367
368  build() {
369    Column() {
370      Button('change message')
371        .onClick(() => {
372          this.message++;
373        })
374      Navigation(this.pageInfo) {
375        Column() {
376          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
377            .width('80%')
378            .height(40)
379            .margin(20)
380            .onClick(() => {
381              this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack.
382            })
383        }
384      }.title('NavIndex')
385      .navDestination(this.PageMap)
386      .mode(NavigationMode.Stack)
387    }
388  }
389}
390
391@Component
392struct pageOneStack {
393  @Consume('pageInfo') pageInfo: NavPathStack;
394  @State index: number = 1;
395  @Link message: number;
396  @Link logNumber: number;
397
398  build() {
399    NavDestination() {
400      Column() {
401        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
402        Text("cur stack size:" + `${this.pageInfo.size()}`)
403          .fontSize(30)
404          .fontWeight(FontWeight.Bold)
405        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
406          .width('80%')
407          .height(40)
408          .margin(20)
409          .onClick(() => {
410            this.pageInfo.pushPathByName('pageTwo', null);
411          })
412        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
413          .width('80%')
414          .height(40)
415          .margin(20)
416          .onClick(() => {
417            this.pageInfo.pop();
418          })
419      }.width('100%').height('100%')
420    }.title('pageOne')
421    .onBackPressed(() => {
422      this.pageInfo.pop();
423      return true;
424    })
425  }
426}
427
428@Component
429struct pageTwoStack {
430  @Consume('pageInfo') pageInfo: NavPathStack;
431  @State index: number = 2;
432  @Link message: number;
433  @Link logNumber: number;
434
435  build() {
436    NavDestination() {
437      Column() {
438        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
439        Text("cur stack size:" + `${this.pageInfo.size()}`)
440          .fontSize(30)
441          .fontWeight(FontWeight.Bold)
442        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
443          .width('80%')
444          .height(40)
445          .margin(20)
446          .onClick(() => {
447            this.pageInfo.pushPathByName('pageThree', null);
448          })
449        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
450          .width('80%')
451          .height(40)
452          .margin(20)
453          .onClick(() => {
454            this.pageInfo.pop();
455          })
456      }.width('100%').height('100%')
457    }.title('pageTwo')
458    .onBackPressed(() => {
459      this.pageInfo.pop();
460      return true;
461    })
462  }
463}
464
465@Component
466struct pageThreeStack {
467  @Consume('pageInfo') pageInfo: NavPathStack;
468  @State index: number = 3;
469  @Link message: number;
470  @Link logNumber: number;
471
472  build() {
473    NavDestination() {
474      Column() {
475        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
476        Text("cur stack size:" + `${this.pageInfo.size()}`)
477          .fontSize(30)
478          .fontWeight(FontWeight.Bold)
479        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
480          .width('80%')
481          .height(40)
482          .margin(20)
483          .onClick(() => {
484            this.pageInfo.pushPathByName('pageOne', null);
485          })
486        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
487          .width('80%')
488          .height(40)
489          .margin(20)
490          .onClick(() => {
491            this.pageInfo.pop();
492          })
493      }.width('100%').height('100%')
494    }.title('pageThree')
495    .onBackPressed(() => {
496      this.pageInfo.pop();
497      return true;
498    })
499  }
500}
501
502@Component({ freezeWhenInactive: true })
503struct NavigationContentMsgStack {
504  @Link @Watch("info") message: number;
505  @Link index: number;
506  @Link logNumber: number;
507
508  info() {
509    console.info(`freeze-test NavigationContent message callback ${this.message}`);
510    console.info(`freeze-test ---- called by content ${this.index}`);
511    this.logNumber++;
512  }
513
514  build() {
515    Column() {
516      Text("msg:" + `${this.message}`)
517        .fontSize(30)
518        .fontWeight(FontWeight.Bold)
519      Text("log number:" + `${this.logNumber}`)
520        .fontSize(30)
521        .fontWeight(FontWeight.Bold)
522    }
523  }
524}
525```
526
527In the preceding example:
528
5291. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **info** method of the **MyNavigationTestStack** component being displayed is called.
530
5312. When **Next Page** is clicked, **PageOne** is displayed, and the **PageOneStack** node is created.
532
5333. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageOneStack** is called.
534
5354. When **Next Page** is clicked again, **PageTwo** is displayed, and the **pageTwoStack** node is created.
536
5375. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called.
538
5396. When **Next Page** is clicked again, **PageThree** is displayed, and the **pageThreeStack** node is created.
540
5417. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageThreeStack** is called.
542
5438. When **Back Page** is clicked, **PageTwo** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called.
544
5459. When **Back Page** is clicked again, **PageOne** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageOneStack** is called.
546
54710. When **Back Page** is clicked again, the initial page is displayed, and no method is called.
548
549![navigation-freeze.gif](figures/navigation-freeze.gif)
550
551### Reusing Components
552
553[Components reuse](./arkts-reusable.md) existing nodes in the cache pool instead of creating new nodes to optimize UI performance and improve application smoothness. Although the nodes in the reuse pool are not displayed in the UI component tree, the change of the state variable still triggers the UI re-render. To solve the problem that components in the reuse pool are re-rendered abnormally, you can perform component freezing.
554
555#### Mixed Use of Component Reuse, if, and Component Freezing
556The following example shows that when the state variable bound to the **if** component changes to **false**, the detach of **ChildComponent** is triggered. Because **ChildComponent** is marked as component reuse, it is not destroyed but enters the reuse pool, in this case, if the component freezing is enabled at the same time, the component will not be re-rendered in the reuse pool.
557The procedure is as follows:
5581. Click **change flag** and change the value of **flag** to **false**.
559    -  When **ChildComponent** marked with \@Reusable is detached, it is not destroyed. Instead, it enters the reuse pool, triggers the **aboutToRecycle** lifecycle, and sets the component state to inactive.
560    - **ChildComponent** also enables component freezing. When **ChildComponent** is in the inactive state, it does not respond to any UI re-render caused by state variable changes.
5612. Click **change desc** to trigger the change of the member variable **desc** of **Page**.
562    - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**.
563    - However, **ChildComponent** is in the inactive state and the component freezing is enabled. Therefore, the change does not trigger the callback of @Watch('descChange') and the re-render of the `ChildComponent` UI. If component freezing is not enabled, the current @Watch('descChange') callback is returned immediately, and **ChildComponent** in the reuse pool is re-rendered accordingly.
5643. Click **change flag** again and change the value of **flag** to **true**.
565    - **ChildComponent** is attached to the component tree from the reuse pool.
566    - Return the **aboutToReuse** lifecycle callback and synchronize the latest **count** value to **ChildComponent**. The value of **desc** is synchronized from @State to @Link. Therefore, you do not need to manually assign a value to **aboutToReuse**.
567    - Set **ChildComponent** to the active state and re-render the component that is not re-rendered when **ChildComponent** is inactive, for example, **Text (ChildComponent desc: ${this.desc})**.
568
569
570```ts
571@Reusable
572@Component({freezeWhenInactive: true})
573struct ChildComponent {
574  @Link @Watch('descChange') desc: string;
575  @State count: number = 0;
576  descChange() {
577    console.info(`ChildComponent messageChange ${this.desc}`);
578  }
579
580  aboutToReuse(params: Record<string, ESObject>): void {
581    this.count = params.count as number;
582  }
583
584  aboutToRecycle(): void {
585    console.info(`ChildComponent has been recycled`);
586  }
587  build() {
588    Column() {
589      Text(`ChildComponent desc: ${this.desc}`)
590        .fontSize(20)
591      Text(`ChildComponent count ${this.count}`)
592        .fontSize(20)
593    }.border({width: 2, color: Color.Pink})
594  }
595}
596
597@Entry
598@Component
599struct Page {
600  @State desc: string = 'Hello World';
601  @State flag: boolean = true;
602  @State count: number = 0;
603  build() {
604    Column() {
605      Button(`change desc`).onClick(() => {
606        this.desc += '!';
607      })
608      Button(`change flag`).onClick(() => {
609        this.count++;
610        this.flag =! this.flag;
611      })
612      if (this.flag) {
613        ChildComponent({desc: this.desc, count: this.count})
614      }
615    }
616    .height('100%')
617  }
618}
619```
620#### Mixed Use of LazyForEach, Component Reuse, and Component Freezing
621In the scrolling scenario of a long list with a large amount of data, you can use **LazyForEach** to create components as required. In addition, you can reuse components to reduce the overhead caused by component creation and destruction during scrolling.
622However, if you set <!--RP2-->[reuseId](../performance/component-recycle.md#available-apis)<!--RP2End--> based on the reuse type or assign a large value to **cacheCount** to ensure the scrolling performance, more nodes will be cached in the reuse pool or **LazyForEach**.
623In this case, if you trigger the re-render of all subnodes in **List**, the number of re-renders is too large. In this case, you can freeze the component.
624
625Example:
6261. Swipe the list to the position whose index is 14. There are 15 **ChildComponent** in the visible area on the current page.
6272. During swiping:
628    - **ChildComponent** in the upper part of the list is swiped out of the visible area. In this case, **ChildComponent** enters the cache area of LazyForEach and is set to inactive. After the component slides out of the **LazyForEach** cache area, the component is not destructed and enters the reuse pool because the component is marked for reuse. In this case, the component is set to inactive again.
629    - The cache node of **LazyForEach** at the bottom of the list enters the list. In this case, the system attempts to create a node to enter the cache of **LazyForEach**. If a node that can be reused is found, the system takes out the existing node from the reuse pool and triggers the **aboutToReuse** lifecycle callback, in this case, the node enters the cache area of **LazyForEach** and the state of the node is still inactive.
6303. Click **change desc** to trigger the change of the member variable **desc** of **Page**.
631    - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**.
632    - **ChildComponent** in the invisible area is in the inactive state, and the component freezing is enabled. Therefore, this change triggers the @Watch('descChange') callback of the 15 nodes in the visible area and re-renders these nodes. Nodes cached in **LazyForEach** and the reuse pool are not re-rendered, and the \@Watch callback is not triggered.
633
634
635For details, see the following.
636![freeze](./figures/freezeResuable.png)
637You can listen for the changes by \@Trace, only 15 **ChildComponent** nodes are re-rendered.
638![freeze](./figures/traceWithFreeze.png)
639A complete sample code is as follows:
640```ts
641import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
642// Basic implementation of IDataSource used to listening for data.
643class BasicDataSource implements IDataSource {
644  private listeners: DataChangeListener[] = [];
645  private originDataArray: string[] = [];
646
647  public totalCount(): number {
648    return 0;
649  }
650
651  public getData(index: number): string {
652    return this.originDataArray[index];
653  }
654
655  // This method is called by the framework to add a listener to the LazyForEach data source.
656  registerDataChangeListener(listener: DataChangeListener): void {
657    if (this.listeners.indexOf(listener) < 0) {
658      console.info('add listener');
659      this.listeners.push(listener);
660    }
661  }
662
663  // This method is called by the framework to remove the listener from the LazyForEach data source.
664  unregisterDataChangeListener(listener: DataChangeListener): void {
665    const pos = this.listeners.indexOf(listener);
666    if (pos >= 0) {
667      console.info('remove listener');
668      this.listeners.splice(pos, 1);
669    }
670  }
671
672  // Notify LazyForEach that all child components need to be reloaded.
673  notifyDataReload(): void {
674    this.listeners.forEach(listener => {
675      listener.onDataReloaded();
676    })
677  }
678
679  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
680  notifyDataAdd(index: number): void {
681    this.listeners.forEach(listener => {
682      listener.onDataAdd(index);
683    })
684  }
685
686  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
687  notifyDataChange(index: number): void {
688    this.listeners.forEach(listener => {
689      listener.onDataChange(index);
690    })
691  }
692
693  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
694  notifyDataDelete(index: number): void {
695    this.listeners.forEach(listener => {
696      listener.onDataDelete(index);
697    })
698  }
699
700  // Notify LazyForEach that data needs to be swapped between the from and to positions.
701  notifyDataMove(from: number, to: number): void {
702    this.listeners.forEach(listener => {
703      listener.onDataMove(from, to);
704    })
705  }
706}
707
708class MyDataSource extends BasicDataSource {
709  private dataArray: string[] = [];
710
711  public totalCount(): number {
712    return this.dataArray.length;
713  }
714
715  public getData(index: number): string {
716    return this.dataArray[index];
717  }
718
719  public addData(index: number, data: string): void {
720    this.dataArray.splice(index, 0, data);
721    this.notifyDataAdd(index);
722  }
723
724  public pushData(data: string): void {
725    this.dataArray.push(data);
726    this.notifyDataAdd(this.dataArray.length - 1);
727  }
728}
729
730@Reusable
731@Component({freezeWhenInactive: true})
732struct ChildComponent {
733  @Link @Watch('descChange') desc: string;
734  @State item: string = '';
735  @State index: number = 0;
736  descChange() {
737    console.info(`ChildComponent messageChange ${this.desc}`);
738  }
739
740  aboutToReuse(params: Record<string, ESObject>): void {
741    this.item = params.item;
742    this.index = params.index;
743  }
744
745  aboutToRecycle(): void {
746    console.info(`ChildComponent has been recycled`);
747  }
748  build() {
749    Column() {
750      Text(`ChildComponent index: ${this.index} item: ${this.item}`)
751        .fontSize(20)
752      Text(`desc: ${this.desc}`)
753        .fontSize(20)
754    }.border({width: 2, color: Color.Pink})
755  }
756}
757
758@Entry
759@Component
760struct Page {
761  @State desc: string = 'Hello World';
762  private data: MyDataSource = new MyDataSource();
763
764  aboutToAppear() {
765    for (let i = 0; i < 50; i++) {
766      this.data.pushData(`Hello ${i}`);
767    }
768  }
769
770  build() {
771    Column() {
772      Button(`change desc`).onClick(() => {
773        hiTraceMeter.startTrace('change decs', 1);
774        this.desc += '!';
775        hiTraceMeter.finishTrace('change decs', 1);
776      })
777      List({ space: 3 }) {
778        LazyForEach(this.data, (item: string, index: number) => {
779          ListItem() {
780            ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0")
781          }
782        }, (item: string) => item)
783      }.cachedCount(5)
784    }
785    .height('100%')
786  }
787}
788```
789#### Mixed Use of LazyForEach, if, Component Reuse, and Component Freezing
790
791 Under the same parent custom component, reusable nodes may enter the reuse pool in different ways. For example:
792- Detaching from the cache area of LazyForEach by swiping.
793- Notifying the subnodes to detach by switching the if condition.
794
795In the following example:
7961. When you swipe the list to the position whose index is 14, there are 10 **ChildComponent**s in the visible area on the page, among which nine are subnodes of **LazyForEach** and one is a subnode of **if**.
7972. Click **change flag**. The **if** condition is changed to **false**, and its subnode **ChildComponent** enters the reuse pool. Nine nodes are displayed on the page.
7983. In this case, the nodes detached through **LazyForEach** or **if** all enter the reuse pool under the **Page** node.
7994. Click **change desc** to update only the nine **ChildComponent** nodes on the page. For details, see figures below.
8005. Click **change flag** again. The **if** condition changes to **true**, and **ChildComponent** is attached from the reuse pool to the component tree again. The state of **ChildComponent** changes to active.
8016. Click **change desc** again. The nodes attached through **if** and **LazyForEach** from the reuse pool can be re-rendered.
802
803Trace for component freezing enabled
804
805![traceWithFreezeLazyForeachAndIf](./figures/traceWithFreezeLazyForeachAndIf.png)
806
807Trace for component freezing disabled
808
809![traceWithFreezeLazyForeachAndIf](./figures/traceWithLazyForeachAndIf.png)
810
811
812A complete example is as follows:
813```
814import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
815class BasicDataSource implements IDataSource {
816  private listeners: DataChangeListener[] = [];
817  private originDataArray: string[] = [];
818
819  public totalCount(): number {
820    return 0;
821  }
822
823  public getData(index: number): string {
824    return this.originDataArray[index];
825  }
826
827  // This method is called by the framework to add a listener to the LazyForEach data source.
828  registerDataChangeListener(listener: DataChangeListener): void {
829    if (this.listeners.indexOf(listener) < 0) {
830      console.info('add listener');
831      this.listeners.push(listener);
832    }
833  }
834
835  // This method is called by the framework to remove the listener from the LazyForEach data source.
836  unregisterDataChangeListener(listener: DataChangeListener): void {
837    const pos = this.listeners.indexOf(listener);
838    if (pos >= 0) {
839      console.info('remove listener');
840      this.listeners.splice(pos, 1);
841    }
842  }
843
844  // Notify LazyForEach that all child components need to be reloaded.
845  notifyDataReload(): void {
846    this.listeners.forEach(listener => {
847      listener.onDataReloaded();
848    })
849  }
850
851  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
852  notifyDataAdd(index: number): void {
853    this.listeners.forEach(listener => {
854      listener.onDataAdd(index);
855    })
856  }
857
858  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
859  notifyDataChange(index: number): void {
860    this.listeners.forEach(listener => {
861      listener.onDataChange(index);
862    })
863  }
864
865  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
866  notifyDataDelete(index: number): void {
867    this.listeners.forEach(listener => {
868      listener.onDataDelete(index);
869    })
870  }
871
872  // Notify LazyForEach that data needs to be swapped between the from and to positions.
873  notifyDataMove(from: number, to: number): void {
874    this.listeners.forEach(listener => {
875      listener.onDataMove(from, to);
876    })
877  }
878}
879
880class MyDataSource extends BasicDataSource {
881  private dataArray: string[] = [];
882
883  public totalCount(): number {
884    return this.dataArray.length;
885  }
886
887  public getData(index: number): string {
888    return this.dataArray[index];
889  }
890
891  public addData(index: number, data: string): void {
892    this.dataArray.splice(index, 0, data);
893    this.notifyDataAdd(index);
894  }
895
896  public pushData(data: string): void {
897    this.dataArray.push(data);
898    this.notifyDataAdd(this.dataArray.length - 1);
899  }
900}
901
902@Reusable
903@Component({freezeWhenInactive: true})
904struct ChildComponent {
905  @Link @Watch('descChange') desc: string;
906  @State item: string = '';
907  @State index: number = 0;
908  descChange() {
909    console.info(`ChildComponent messageChange ${this.desc}`);
910  }
911
912  aboutToReuse(params: Record<string, ESObject>): void {
913    this.item = params.item;
914    this.index = params.index;
915  }
916
917  aboutToRecycle(): void {
918    console.info(`ChildComponent has been recycled`);
919  }
920  build() {
921    Column() {
922      Text(`ChildComponent index: ${this.index} item: ${this.item}`)
923        .fontSize(20)
924      Text(`desc: ${this.desc}`)
925        .fontSize(20)
926    }.border({width: 2, color: Color.Pink})
927  }
928}
929
930@Entry
931@Component
932struct Page {
933  @State desc: string = 'Hello World';
934  @State flag: boolean = true;
935  private data: MyDataSource = new MyDataSource();
936
937  aboutToAppear() {
938    for (let i = 0; i < 50; i++) {
939      this.data.pushData(`Hello ${i}`);
940    }
941  }
942
943  build() {
944    Column() {
945      Button(`change desc`).onClick(() => {
946        hiTraceMeter.startTrace('change decs', 1);
947        this.desc += '!';
948        hiTraceMeter.finishTrace('change decs', 1);
949      })
950
951      Button(`change flag`).onClick(() => {
952        hiTraceMeter.startTrace('change flag', 1);
953        this.flag = !this.flag;
954        hiTraceMeter.finishTrace('change flag', 1);
955      })
956
957      List({ space: 3 }) {
958        LazyForEach(this.data, (item: string, index: number) => {
959          ListItem() {
960            ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0")
961          }
962        }, (item: string) => item)
963      }
964      .cachedCount(5)
965      .height('60%')
966
967      if (this.flag) {
968        ChildComponent({index: -1, item: 'Hello', desc: this.desc}).reuseId( "1")
969      }
970    }
971    .height('100%')
972  }
973}
974```
975
976### Mixing the Use of Components
977
978In the scenario where mixed use of component freezing is supported, the freezing behavior varies according to the API version. Set the component freezing flag for the parent component. In API version 17 or earlier, when the parent component is unfrozen, all nodes of its child components are unfrozen. Since API version 18, when the parent component is unfrozen, only the on-screen nodes of the child component are unfrozen.
979
980#### Mixed Use of Navigation and TabContent
981
982The sample code is as follows:
983
984```ts
985// index.ets
986@Component
987struct ChildOfParamComponent {
988  @Prop @Watch('onChange') child_val: number;
989
990  onChange() {
991    console.log(`Appmonitor ChildOfParamComponent: child_val changed:${this.child_val}`);
992  }
993
994  build() {
995    Column() {
996      Text(`Child Param: ${this.child_val}`);
997    }
998  }
999}
1000
1001@Component
1002struct ParamComponent {
1003  @Prop @Watch('onChange')  paramVal: number;
1004
1005  onChange() {
1006    console.log(`Appmonitor ParamComponent: paramVal changed:${this.paramVal}`);
1007  }
1008
1009  build() {
1010    Column() {
1011      Text(`val: ${this.paramVal}`)
1012      ChildOfParamComponent({child_val: this.paramVal});
1013    }
1014  }
1015}
1016
1017
1018
1019@Component
1020struct DelayComponent {
1021  @Prop @Watch('onChange') delayVal: number;
1022
1023  onChange() {
1024    console.log(`Appmonitor ParamComponent: delayVal changed:${this.delayVal}`);
1025  }
1026
1027
1028  build() {
1029    Column() {
1030      Text(`Delay Param: ${this.delayVal}`);
1031    }
1032  }
1033}
1034
1035@Component ({freezeWhenInactive: true})
1036struct TabsComponent {
1037  private controller: TabsController = new TabsController();
1038  @State @Watch('onChange') tabState: number = 47;
1039
1040  onChange() {
1041    console.log(`Appmonitor TabsComponent: tabState changed:${this.tabState}`);
1042  }
1043
1044  build() {
1045    Column({space: 10}) {
1046      Button(`Incr state ${this.tabState}`)
1047        .fontSize(25)
1048        .onClick(() => {
1049          console.log('Button increment state value');
1050          this.tabState = this.tabState + 1;
1051        })
1052
1053      Tabs({ barPosition: BarPosition.Start, index: 0, controller: this.controller}) {
1054        TabContent() {
1055          ParamComponent({paramVal: this.tabState});
1056        }.tabBar('Update')
1057        TabContent() {
1058          DelayComponent({delayVal: this.tabState});
1059        }.tabBar('DelayUpdate')
1060      }
1061      .vertical(false)
1062      .scrollable(true)
1063      .barMode(BarMode.Fixed)
1064      .barWidth(400).barHeight(150).animationDuration(400)
1065      .width('100%')
1066      .height(200)
1067      .backgroundColor(0xF5F5F5)
1068    }
1069  }
1070}
1071
1072@Entry
1073@Component
1074struct MyNavigationTestStack {
1075  @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
1076
1077  @Builder
1078  PageMap(name: string) {
1079    if (name === 'pageOne') {
1080      pageOneStack()
1081    } else if (name === 'pageTwo') {
1082      pageTwoStack()
1083    }
1084  }
1085
1086  build() {
1087    Column() {
1088      Navigation(this.pageInfo) {
1089        Column() {
1090          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
1091            .width('80%')
1092            .height(40)
1093            .margin(20)
1094            .onClick(() => {
1095              this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack.
1096            })
1097        }
1098      }.title('NavIndex')
1099      .navDestination(this.PageMap)
1100      .mode(NavigationMode.Stack)
1101    }
1102  }
1103}
1104
1105@Component
1106struct pageOneStack {
1107  @Consume('pageInfo') pageInfo: NavPathStack;
1108
1109  build() {
1110    NavDestination() {
1111      Column() {
1112        TabsComponent();
1113
1114        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
1115          .width('80%')
1116          .height(40)
1117          .margin(20)
1118          .onClick(() => {
1119            this.pageInfo.pushPathByName('pageTwo', null);
1120          })
1121      }.width('100%').height('100%')
1122    }.title('pageOne')
1123    .onBackPressed(() => {
1124      this.pageInfo.pop();
1125      return true;
1126    })
1127  }
1128}
1129
1130@Component
1131struct pageTwoStack {
1132  @Consume('pageInfo') pageInfo: NavPathStack;
1133
1134  build() {
1135    NavDestination() {
1136      Column() {
1137        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
1138          .width('80%')
1139          .height(40)
1140          .margin(20)
1141          .onClick(() => {
1142            this.pageInfo.pop();
1143          })
1144      }.width('100%').height('100%')
1145    }.title('pageTwo')
1146    .onBackPressed(() => {
1147      this.pageInfo.pop();
1148      return true;
1149    })
1150  }
1151}
1152```
1153
1154Final effect
1155
1156![freeze](figures/freeze_tabcontent.gif)
1157
1158Click the **Next Page** button to enter the **pageOne** page. There are two tabs on the page and the **Update** tab is displayed by default. Enable component freezing. If the **Tabcontent** tab is not selected, the state variable is not refreshed.
1159
1160Click the **Incr state** button to query **Appmonitor** in the log. Three records are displayed.
1161
1162![freeze](figures/freeze_tabcontent_update.png)
1163
1164Switch to the **DelayUpdate** tab and click the **Incr state** button to query **Appmonitor** in the log. Two records are displayed. The state variable in the **DelayUpdate** tab does not refresh the state variable related to the **Update** tab.
1165
1166![freeze](figures/freeze_tabcontent_delayupdate.png)
1167
1168For API version 17 or earlier:
1169
1170Click **Next page** to enter the next page and then return. The tab is **DelayUpdate** by default. Click **Incr state** to query **Appmonitor** in the log and four records are displayed. When the page route is returned, all tabs of **Tabcontent** are unfrozen.
1171
1172![freeze](figures/freeze_tabcontent_back_api15.png)
1173
1174For API version 18 or later:
1175
1176Click **Next page** to enter the next page and then return. The tab is **DelayUpdate** by default. Click **Incr state** to query **Appmonitor** in the log and two records are displayed. When the page route is returned, only the nodes with the corresponding tabs are unfrozen.
1177
1178![freeze](figures/freeze_tabcontent_back_api16.png)
1179
1180#### Page and LazyForEach
1181
1182When **Navigation** and **TabContent** are used together, the child nodes of **TabContent** are unlocked because the child component is recursively unfrozen from the parent component when the previous page is displayed. In addition, the page lifecycle **OnPageShow** shows a similar behavior. **OnPageShow** sets the root node of the current page to the active state. As a subnode of the page, **TabContent** is also set to the active state. When the screen is turned off or on, the page lifecycles **OnPageHide** and **OnPageShow** are triggered respectively. Therefore, when **LazyForEach** is used on the page, manual screen-off and screen-on can also implement the page routing effect. The sample code is as follows:
1183
1184```ts
1185import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
1186// Basic implementation of IDataSource used to listening for data.
1187class BasicDataSource implements IDataSource {
1188  private listeners: DataChangeListener[] = [];
1189  private originDataArray: string[] = [];
1190
1191  public totalCount(): number {
1192    return 0;
1193  }
1194
1195  public getData(index: number): string {
1196    return this.originDataArray[index];
1197  }
1198
1199  // This method is called by the framework to add a listener to the LazyForEach data source.
1200  registerDataChangeListener(listener: DataChangeListener): void {
1201    if (this.listeners.indexOf(listener) < 0) {
1202      console.info('add listener');
1203      this.listeners.push(listener);
1204    }
1205  }
1206
1207  // This method is called by the framework to remove the listener from the LazyForEach data source.
1208  unregisterDataChangeListener(listener: DataChangeListener): void {
1209    const pos = this.listeners.indexOf(listener);
1210    if (pos >= 0) {
1211      console.info('remove listener');
1212      this.listeners.splice(pos, 1);
1213    }
1214  }
1215
1216  // Notify LazyForEach that all child components need to be reloaded.
1217  notifyDataReload(): void {
1218    this.listeners.forEach(listener => {
1219      listener.onDataReloaded();
1220    })
1221  }
1222
1223  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
1224  notifyDataAdd(index: number): void {
1225    this.listeners.forEach(listener => {
1226      listener.onDataAdd(index);
1227    })
1228  }
1229
1230  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
1231  notifyDataChange(index: number): void {
1232    this.listeners.forEach(listener => {
1233      listener.onDataChange(index);
1234    })
1235  }
1236
1237  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
1238  notifyDataDelete(index: number): void {
1239    this.listeners.forEach(listener => {
1240      listener.onDataDelete(index);
1241    })
1242  }
1243
1244  // Notify LazyForEach that data needs to be swapped between the from and to positions.
1245  notifyDataMove(from: number, to: number): void {
1246    this.listeners.forEach(listener => {
1247      listener.onDataMove(from, to);
1248    })
1249  }
1250}
1251
1252class MyDataSource extends BasicDataSource {
1253  private dataArray: string[] = [];
1254
1255  public totalCount(): number {
1256    return this.dataArray.length;
1257  }
1258
1259  public getData(index: number): string {
1260    return this.dataArray[index];
1261  }
1262
1263  public addData(index: number, data: string): void {
1264    this.dataArray.splice(index, 0, data);
1265    this.notifyDataAdd(index);
1266  }
1267
1268  public pushData(data: string): void {
1269    this.dataArray.push(data);
1270    this.notifyDataAdd(this.dataArray.length - 1);
1271  }
1272}
1273
1274@Reusable
1275@Component({freezeWhenInactive: true})
1276struct ChildComponent {
1277  @State desc: string = '';
1278  @Link @Watch('sumChange') sum: number;
1279
1280  sumChange() {
1281    console.info(`sum: Change ${this.sum}`);
1282  }
1283
1284  aboutToReuse(params: Record<string, Object>): void {
1285    this.desc = params.desc as string;
1286    this.sum = params.sum as number;
1287  }
1288
1289  aboutToRecycle(): void {
1290    console.info(`ChildComponent has been recycled`);
1291  }
1292  build() {
1293    Column() {
1294      Divider()
1295        .color('#ff11acb8')
1296      Text('Child component:' + this.desc)
1297        .fontSize(30)
1298        .fontWeight(30)
1299      Text(`${this.sum}`)
1300        .fontSize(30)
1301        .fontWeight(30)
1302    }
1303  }
1304}
1305
1306@Entry
1307@Component ({freezeWhenInactive: true})
1308struct Page {
1309  private data: MyDataSource = new MyDataSource();
1310  @State sum: number = 0;
1311  @State desc: string = '';
1312
1313  aboutToAppear() {
1314    for (let index = 0; index < 20; index++) {
1315      this.data.pushData(index.toString());
1316    }
1317  }
1318
1319  build() {
1320    Column() {
1321      Button(`add sum`).onClick(() => {
1322        this.sum++;
1323      })
1324        .fontSize(30)
1325        .margin(20)
1326      List() {
1327        LazyForEach(this.data, (item: string) => {
1328          ListItem() {
1329            ChildComponent({desc: item, sum: this.sum});
1330          }
1331          .width('100%')
1332          .height(100)
1333        }, (item: string) => item)
1334      }.cachedCount(5)
1335    }
1336    .height('100%')
1337    .width('100%')
1338  }
1339}
1340```
1341
1342As described in the mixed use scenario, the nodes of **LazyForEach** include the on-screen node and **cachedCount** node.
1343
1344![freeze](figures/freeze_lazyforeach.png)
1345
1346Swipe down **LazyForEach** to add nodes to **cachedCount**. Click the **add sum** button to search for the log "sum: Change." and eight records are displayed.
1347
1348![freeze](figures/freeze_lazyforeach_add.png)
1349
1350For API version 17 or earlier:
1351
1352Turn off and on the screen to trigger **OnPageShow** and then click **add sum**. The number of printed records is equal to the number of on-screen nodes and the **cachedCount** nodes.
1353
1354![freeze](figures/freeze_lazyforeach_api15.png)
1355
1356For API version 18 or later:
1357
1358Turn off and on the screen to trigger **OnPageShow** and then click **add sum**. Only the number of on-screen nodes is displayed, and the **cachedCount** nodes are not unfrozen.
1359
1360![freeze](figures/freeze_lazyforeach_api16.png)
1361
1362## Constraints
1363
1364As shown in the following example, the custom node [BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md) is used in **FreezeBuildNode**. **BuilderNode** can dynamically mount components using commands and component freezing strongly depends on the parent-child relationship to determine whether it is enabled. In this case, if the parent component is frozen and **BuilderNode** is enabled at the middle level of the component tree, the child component of the **BuilderNode** cannot be frozen.
1365
1366```
1367import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
1368
1369// Define a Params class to pass parameters.
1370class Params {
1371  index: number = 0;
1372
1373  constructor(index: number) {
1374    this.index = index;
1375  }
1376}
1377
1378// Define a buildNodeChild component that contains a message attribute and an index attribute.
1379@Component
1380struct buildNodeChild {
1381  @StorageProp("buildNodeTest") @Watch("onMessageUpdated") message: string = "hello world";
1382  @State index: number = 0;
1383
1384  // Call this method when message is updated.
1385  onMessageUpdated() {
1386    console.log(`FreezeBuildNode builderNodeChild message callback func ${this.message},index: ${this.index}`);
1387  }
1388
1389  build() {
1390    Text(`buildNode Child message: ${this.message}`).fontSize(30)
1391  }
1392}
1393
1394// Define a buildText function that receives a Params parameter and constructs a Column component.
1395@Builder
1396function buildText(params: Params) {
1397  Column() {
1398    buildNodeChild({ index: params.index })
1399  }
1400}
1401
1402// Define a TextNodeController class that is inherited from NodeController.
1403class TextNodeController extends NodeController {
1404  private textNode: BuilderNode<[Params]> | null = null;
1405  private index: number = 0;
1406
1407  // The constructor receives an index parameter.
1408  constructor(index: number) {
1409    super();
1410    this.index = index;
1411  }
1412
1413  // Create and return a FrameNode.
1414  makeNode(context: UIContext): FrameNode | null {
1415    this.textNode = new BuilderNode(context);
1416    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index));
1417    return this.textNode.getFrameNode();
1418  }
1419}
1420
1421// Define an index component that contains a message attribute and a data array.
1422@Entry
1423@Component
1424struct Index {
1425  @StorageLink("buildNodeTest") message: string = "hello";
1426  private data: number[] = [0, 1];
1427
1428  build() {
1429    Row() {
1430      Column() {
1431        Button("change").fontSize(30)
1432          .onClick(() => {
1433            this.message += 'a';
1434          })
1435
1436        Tabs() {
1437          ForEach(this.data, (item: number) => {
1438            TabContent() {
1439              FreezeBuildNode({ index: item })
1440            }.tabBar(`tab${item}`)
1441          }, (item: number) => item.toString())
1442        }
1443      }
1444    }
1445    .width('100%')
1446    .height('100%')
1447  }
1448}
1449
1450// Define a FreezeBuildNode component that contains a message attribute and an index attribute.
1451@Component({ freezeWhenInactive: true })
1452struct FreezeBuildNode {
1453  @StorageProp("buildNodeTest") @Watch("onMessageUpdated") message: string = "1111";
1454  @State index: number = 0;
1455
1456  // Call this method when message is updated.
1457  onMessageUpdated() {
1458    console.log(`FreezeBuildNode message callback func ${this.message}, index: ${this.index}`);
1459  }
1460
1461  build() {
1462    NodeContainer(new TextNodeController(this.index))
1463      .width('100%')
1464      .height('100%')
1465      .backgroundColor('#FFF0F0F0')
1466  }
1467}
1468```
1469
1470In the preceding example:
1471
1472Click **Button("change")** to change the value of **message**. The **onMessageUpdated** method registered in @Watch of the **TabContent** component that is being displayed is triggered, and that under the **BuilderNode** node of **TabContent** that is not displayed is also triggered.
1473
1474![builderNode.gif](figures/builderNode.gif)
1475