• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Freezing a Custom Component
2
3When a custom component decorated by @ComponentV2 is inactive, it can be frozen so that its state variable does not respond to updates. That is, the @Monitor decorated method is not called, and the node associated with the state variable is not re-rendered. You can use the **freezeWhenInactive** attribute to specify whether to freeze a custom component. If no parameter is passed in, the feature is disabled. This feature works in following scenarios: page routing, **TabContent**, and **Navigation**.
4
5Before reading this topic, you are advised to read [\@ComponentV2](./arkts-new-componentV2.md).
6
7> **NOTE**
8>
9> Freezing of @ComponentV2 decorated custom component is supported since API version 12.
10>
11> Mixed use of custom component freezing is supported since API version 18.
12>
13> Different from freezing the @Component decorated components, custom components decorated by @ComponentV2 do not support freezing the cached list items in the **LazyForEach** scenario.
14
15
16## Use Scenarios
17
18### Page Routing
19
20> **NOTE**
21>
22> 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).
23
24- When 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.
25For details, see the following.
26
27![freezeInPage](./figures/freezeInPage.png)
28
29Page 1
30
31```ts
32import { router } from '@kit.ArkUI';
33
34@ObservedV2
35export class Book {
36  @Trace name: string = "100";
37
38  constructor(page: string) {
39    this.name = page;
40  }
41}
42
43@Entry
44@ComponentV2({ freezeWhenInactive: true })
45export struct Page1 {
46  @Local bookTest: Book = new Book("A Midsummer Night's Dream");
47
48  @Monitor("bookTest.name")
49  onMessageChange(monitor: IMonitor) {
50    console.log(`The book name change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
51  }
52
53  build() {
54    Column() {
55      Text(`Book name is ${this.bookTest.name}`).fontSize(25)
56      Button('changeBookName').fontSize(25)
57        .onClick(() => {
58          this.bookTest.name = "The Old Man and the Sea";
59        })
60      Button('go to next page').fontSize(25)
61        .onClick(() => {
62          router.pushUrl({ url: 'pages/Page2' });
63          setTimeout(() => {
64            this.bookTest = new Book("Jane Austen oPride and Prejudice");
65          }, 1000)
66        })
67    }
68  }
69}
70```
71
72Page 2
73
74```ts
75import { router } from '@kit.ArkUI';
76
77@Entry
78@ComponentV2
79struct Page2 {
80  build() {
81    Column() {
82      Text(`This is the page2`).fontSize(25)
83      Button('Back')
84        .onClick(() => {
85          router.back();
86        })
87    }
88  }
89}
90```
91
92In the preceding example:
93
941. Click the **changeBookName** button on page 1. The **name** attribute of the **bookTest** variable is changed, and the **onMessageChange** method registered in @Monitor is called.
95
962. Click the **go to next page** button on page 1 to redirect to page 2, and then update the state variable **bookTest** 1s later. When **bookTest** is updated, page 2 is displayed and Page 1 is in the inactive state. The state variable @Local bookTest does not respond to the update. Therefore, the @Monitor is not called, and the node associated with the state variable is not updated.
97The trace diagram is as follows.
98
99![Example Image](./figures/freeze1.png)
100
101
1023. Click **Back**. Page 2 is destroyed, and the state of page 1 changes from inactive to active. The update of the state variable **bookTest** is observed, the **onMessageChange** method registered in @Monitor is called, and the corresponding text content is changed.
103
104![freezeV2Page](./figures/freezeV2page.gif)
105
106
107### TabContent
108
109- You can freeze invisible **TabContent** components in the **Tabs** container so that they do not trigger UI re-rendering.
110
111- 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.
112
113For details, see the following.
114![freezeWithTab](./figures/freezewithTabs.png)
115
116
117```ts
118@Entry
119@ComponentV2
120struct TabContentTest {
121  @Local message: number = 0;
122  @Local data: number[] = [0, 1];
123
124  build() {
125    Row() {
126      Column() {
127        Button('change message').onClick(() => {
128          this.message++;
129        })
130
131        Tabs() {
132          ForEach(this.data, (item: number) => {
133            TabContent() {
134              FreezeChild({ message: this.message, index: item })
135            }.tabBar(`tab${item}`)
136          }, (item: number) => item.toString())
137        }
138      }
139      .width('100%')
140    }
141    .height('100%')
142  }
143}
144
145@ComponentV2({ freezeWhenInactive: true })
146struct FreezeChild {
147  @Param message: number = 0;
148  @Param index: number = 0;
149
150  @Monitor('message') onMessageUpdated(mon: IMonitor) {
151    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`);
152  }
153
154  build() {
155    Text("message" + `${this.message}, index: ${this.index}`)
156      .fontSize(50)
157      .fontWeight(FontWeight.Bold)
158  }
159}
160```
161
162In the preceding example:
163
1641. When **change message** is clicked, the value of **message** changes, and the @Monitor decorated **onMessageUpdated** method of the **TabContent** component being displayed is called.
165
1662. When **tab1** in **TabBar** is clicked to switch to another **TabContent** component, the component switches from inactive to active, and the corresponding @Monitor decorated **onMessageUpdated** method is called.
167
1683. When **change message** is clicked again, the value of **message** changes, and only the @Monitor decorated **onMessageUpdated** method of the **TabContent** component being displayed is called. Other inactive **TabContent** components do not trigger @Monitor.
169
170![TabContent.gif](figures/TabContent.gif)
171
172
173### Navigation
174
175- 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 @Monitor callback is triggered to re-render the page.
176
177```ts
178@Entry
179@ComponentV2
180struct MyNavigationTestStack {
181  @Provider('pageInfo') pageInfo: NavPathStack = new NavPathStack();
182  @Local message: number = 0;
183
184  @Monitor('message') info() {
185    console.info(`freeze-test MyNavigation message callback ${this.message}`);
186  }
187
188  @Builder
189  PageMap(name: string) {
190    if (name === 'pageOne') {
191      pageOneStack({ message: this.message })
192    } else if (name === 'pageTwo') {
193      pageTwoStack({ message: this.message })
194    } else if (name === 'pageThree') {
195      pageThreeStack({ message: this.message })
196    }
197  }
198
199  build() {
200    Column() {
201      Button('change message')
202        .onClick(() => {
203          this.message++;
204        })
205      Navigation(this.pageInfo) {
206        Column() {
207          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
208            .onClick(() => {
209              this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack.
210            })
211        }
212      }.title('NavIndex')
213      .navDestination(this.PageMap)
214      .mode(NavigationMode.Stack)
215    }
216  }
217}
218
219@ComponentV2
220struct pageOneStack {
221  @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
222  @Local index: number = 1;
223  @Param message: number = 0;
224
225  build() {
226    NavDestination() {
227      Column() {
228        NavigationContentMsgStack({ message: this.message, index: this.index })
229        Text("cur stack size:" + `${this.pageInfo.size()}`)
230          .fontSize(30)
231        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
232          .onClick(() => {
233            this.pageInfo.pushPathByName('pageTwo', null);
234          })
235        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
236          .onClick(() => {
237            this.pageInfo.pop();
238          })
239      }.width('100%').height('100%')
240    }.title('pageOne')
241    .onBackPressed(() => {
242      this.pageInfo.pop();
243      return true;
244    })
245  }
246}
247
248@ComponentV2
249struct pageTwoStack {
250  @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
251  @Local index: number = 2;
252  @Param message: number = 0;
253
254  build() {
255    NavDestination() {
256      Column() {
257        NavigationContentMsgStack({ message: this.message, index: this.index })
258        Text("cur stack size:" + `${this.pageInfo.size()}`)
259          .fontSize(30)
260        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
261          .onClick(() => {
262            this.pageInfo.pushPathByName('pageThree', null);
263          })
264        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
265          .onClick(() => {
266            this.pageInfo.pop();
267          })
268      }
269    }.title('pageTwo')
270    .onBackPressed(() => {
271      this.pageInfo.pop();
272      return true;
273    })
274  }
275}
276
277@ComponentV2
278struct pageThreeStack {
279  @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
280  @Local index: number = 3;
281  @Param message: number = 0;
282
283  build() {
284    NavDestination() {
285      Column() {
286        NavigationContentMsgStack({ message: this.message, index: this.index })
287        Text("cur stack size:" + `${this.pageInfo.size()}`)
288          .fontSize(30)
289        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
290          .height(40)
291          .onClick(() => {
292            this.pageInfo.pushPathByName('pageOne', null);
293          })
294        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
295          .height(40)
296          .onClick(() => {
297            this.pageInfo.pop();
298          })
299      }
300    }.title('pageThree')
301    .onBackPressed(() => {
302      this.pageInfo.pop();
303      return true;
304    })
305  }
306}
307
308@ComponentV2({ freezeWhenInactive: true })
309struct NavigationContentMsgStack {
310  @Param message: number = 0;
311  @Param index: number = 0;
312
313  @Monitor('message') info() {
314    console.info(`freeze-test NavigationContent message callback ${this.message}`);
315    console.info(`freeze-test ---- called by content ${this.index}`);
316  }
317
318  build() {
319    Column() {
320      Text("msg:" + `${this.message}`)
321        .fontSize(30)
322    }
323  }
324}
325```
326
327In the preceding example:
328
3291. When **change message** is clicked, the value of **message** changes, and the @Monitor decorated **info** method of the **MyNavigationTestStack** component being displayed is called.
330
3312. When **Next Page** is clicked, the page is switched to **PageOne** and the **pageOneStack** node is created.
332
3333. When **change message** is clicked again, the value of **message** changes, and only the @Monitor decorated **info** method of the **NavigationContentMsgStack** child component in **pageOneStack** is called.
334
3354. When **Next Page** is clicked, the page is switched to **PageTwo** and the **pageTwoStack** node is created. The state of the **pageOneStack** node changes from active to inactive.
336
3375. When **change message** is clicked again, the value of **message** changes, and only the @Monitor decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called. The child custom component in **NavDestination** that is not at the top of the navigation routing stack is in the inactive state. The @Monitor method is not triggered.
338
3396. When **Next Page** is clicked, the page is switched to **PageThree** and the **pageThreeStack** node is created. The state of the **pageTwoStack** node changes from active to inactive.
340
3417. When **change message** is clicked again, the value of **message** changes, and only the @Monitor decorated **info** method of the **NavigationContentMsgStack** child component in **pageThreeStack** is called. The child custom component in **NavDestination** that is not at the top of the navigation routing stack is in the inactive state. The @Monitor method is not triggered.
342
3438. Click **Back Page** to return to **PageTwo**. The state of the **pageTwoStack** node changes from inactive to active, and the **info** method registered in @Monitor of the **NavigationContentMsgStack** child component is triggered.
344
3459. Click **Back Page** again to return to **PageOne**. The state of the **pageOneStack** node changes from inactive to active, and the **info** method registered in @Monitor of the **NavigationContentMsgStack** child component is triggered.
346
34710. When **Back Page** is clicked, the page is switched to the initial page.
348
349![navigation-freeze.gif](figures/navigation-freeze.gif)
350
351### Repeat virtualScroll
352
353> **NOTE**
354>
355> Repeat virtualScroll supports custom component freezing since API version 18.
356
357Freeze the custom components in the Repeat virtualScroll cache pool to avoid unnecessary component re-renders. You are advised to read [Child Component Rendering Logic](./arkts-new-rendering-control-repeat.md#child-component-rendering-logic-1) of virtualScroll in advance.
358
359```ts
360@Entry
361@ComponentV2
362struct RepeatVirtualScrollFreeze {
363  @Local simpleList: Array<string> = [];
364  @Local bgColor: Color = Color.Pink;
365
366  aboutToAppear(): void {
367    for (let i = 0; i < 7; i++) {
368      this.simpleList.push(`item${i}`);
369    }
370  }
371
372  build() {
373    Column() {
374      Row() {
375        Button(`Reduce length to 5`)
376          .onClick(() => {
377            this.simpleList = this.simpleList.slice(0, 5);
378          })
379        Button(`Change bgColor`)
380          .onClick(() => {
381            this.bgColor = this.bgColor == Color.Pink ? Color.Blue : Color.Pink;
382          })
383      }
384
385      List() {
386        Repeat(this.simpleList)
387          .each((obj: RepeatItem<string>) => {
388          })
389          .key((item: string, index: number) => item)
390          .virtualScroll({ totalCount: this.simpleList.length })
391          .templateId(() => `a`)
392          .template(`a`, (ri) => {
393            ChildComponent({
394              message: ri.item,
395              bgColor: this.bgColor
396            })
397          }, { cachedCount: 2 })
398      }
399      .cachedCount(0)
400      .height(500)
401    }
402    .height(`100%`)
403  }
404}
405
406// Enable component freezing.
407@ComponentV2({ freezeWhenInactive: true })
408struct ChildComponent {
409  @Param @Require message: string = ``;
410  @Param @Require bgColor: Color = Color.Pink;
411  @Monitor(`bgColor`)
412  onBgColorChange(monitor: IMonitor) {
413    // When the bgColor changes, the components in the cache pool are not re-rendered and no log is printed.
414    console.log(`repeat---bgColor change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
415  }
416
417  build() {
418    Text(`[a]: ${this.message}`)
419      .fontSize(50)
420      .backgroundColor(this.bgColor)
421  }
422}
423```
424
425In the preceding example:
426
427After you click **Reduce length to 5**, the two removed components enter the **Repeat** cache pool. Then, click **Change bgColor** to change the value of **bgColor** to trigger node re-rendering.
428
429If **freezeWhenInactive** is set to **true**, only the **onBgColorChange** method decorated by @Monitor in the remaining nodes is triggered. In the example, the five nodes are re-rendered and five logs are printed. The nodes in the cache pool are not re-rendered.
430
431![freeze_repeat_L2.gif](figures/freeze_repeat_L2.gif)
432
433```ts
434// Disable component freezing.
435@ComponentV2({ freezeWhenInactive: false })
436struct ChildComponent {
437  @Param @Require message: string = ``;
438  @Param @Require bgColor: Color = Color.Pink;
439  @Monitor(`bgColor`)
440  onBgColorChange(monitor: IMonitor) {
441    // When the bgColor changes, components in the cache pool are re-rendered and logs are printed.
442    console.log(`repeat---bgColor change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
443  }
444
445  build() {
446    Text(`[a]: ${this.message}`)
447      .fontSize(50)
448      .backgroundColor(this.bgColor)
449  }
450}
451```
452
453**freezeWhenInactive** is set to **false** to disable component freezing. If **freezeWhenInactive** is not specified, component freezing is disabled by default. The **onBgColorChange** method decorated by @Monitor in the remaining nodes and cache pool nodes is triggered, that is, seven nodes are re-rendered and seven logs are printed.
454
455![freeze_repeat_L2_unfreeze.gif](figures/freeze_repeat_L2_unfreeze.gif)
456
457### Mixed Use of Component Freezing
458
459In 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. For details, see [Mixing the Use of Components](./arkts-custom-components-freeze.md#mixing-the-use-of-components).
460
461#### Mixing Use of Navigation and TabContent
462
463```ts
464@ComponentV2
465struct ChildOfParamComponent {
466  @Require @Param child_val: number;
467
468  @Monitor('child_val') onChange(m: IMonitor) {
469    console.log(`Appmonitor ChildOfParamComponent: changed ${m.dirty[0]}: ${m.value()?.before} -> ${m.value()?.now}`);
470  }
471
472  build() {
473    Column() {
474      Text(`Child Param: ${this.child_val}`);
475    }
476  }
477}
478
479@ComponentV2
480struct ParamComponent {
481  @Require @Param val: number;
482
483  @Monitor('val') onChange(m: IMonitor) {
484    console.log(`Appmonitor ParamComponent: changed ${m.dirty[0]}: ${m.value()?.before} -> ${m.value()?.now}`);
485  }
486
487  build() {
488    Column() {
489      Text(`val: ${this.val}`);
490      ChildOfParamComponent({child_val: this.val});
491    }
492  }
493}
494
495@ComponentV2
496struct DelayComponent {
497  @Require @Param delayVal1: number;
498
499  @Monitor('delayVal1') onChange(m: IMonitor) {
500    console.log(`Appmonitor DelayComponent: changed ${m.dirty[0]}: ${m.value()?.before} -> ${m.value()?.now}`);
501  }
502
503  build() {
504    Column() {
505      Text(`Delay Param: ${this.delayVal1}`);
506    }
507  }
508}
509
510@ComponentV2 ({freezeWhenInactive: true})
511struct TabsComponent {
512  private controller: TabsController = new TabsController();
513  @Local tabState: number = 47;
514
515  @Monitor('tabState') onChange(m: IMonitor) {
516    console.log(`Appmonitor TabsComponent: changed ${m.dirty[0]}: ${m.value()?.before} -> ${m.value()?.now}`);
517  }
518
519  build() {
520    Column({space: 10}) {
521      Button(`Incr state ${this.tabState}`)
522        .fontSize(25)
523        .onClick(() => {
524          console.log('Button increment state value');
525          this.tabState = this.tabState + 1;
526        })
527
528      Tabs({ barPosition: BarPosition.Start, index: 0, controller: this.controller}) {
529        TabContent() {
530          ParamComponent({val: this.tabState});
531        }.tabBar('Update')
532        TabContent() {
533          DelayComponent({delayVal1: this.tabState});
534        }.tabBar('DelayUpdate')
535      }
536      .vertical(false)
537      .scrollable(true)
538      .barMode(BarMode.Fixed)
539      .barWidth(400).barHeight(150).animationDuration(400)
540      .width('100%')
541      .height(200)
542      .backgroundColor(0xF5F5F5)
543    }
544  }
545}
546
547@Entry
548@Component
549struct MyNavigationTestStack {
550  @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
551
552  @Builder
553  PageMap(name: string) {
554    if (name === 'pageOne') {
555      pageOneStack()
556    } else if (name === 'pageTwo') {
557      pageTwoStack()
558    }
559  }
560
561  build() {
562    Column() {
563      Navigation(this.pageInfo) {
564        Column() {
565          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
566            .width('80%')
567            .height(40)
568            .margin(20)
569            .onClick(() => {
570              this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack.
571            })
572        }
573      }.title('NavIndex')
574      .navDestination(this.PageMap)
575      .mode(NavigationMode.Stack)
576    }
577  }
578}
579
580@Component
581struct pageOneStack {
582  @Consume('pageInfo') pageInfo: NavPathStack;
583
584  build() {
585    NavDestination() {
586      Column() {
587        TabsComponent();
588
589        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
590          .width('80%')
591          .height(40)
592          .margin(20)
593          .onClick(() => {
594            this.pageInfo.pushPathByName('pageTwo', null);
595          })
596      }.width('100%').height('100%')
597    }.title('pageOne')
598    .onBackPressed(() => {
599      this.pageInfo.pop();
600      return true;
601    })
602  }
603}
604
605@Component
606struct pageTwoStack {
607  @Consume('pageInfo') pageInfo: NavPathStack;
608
609  build() {
610    NavDestination() {
611      Column() {
612        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
613          .width('80%')
614          .height(40)
615          .margin(20)
616          .onClick(() => {
617            this.pageInfo.pop();
618          })
619      }.width('100%').height('100%')
620    }.title('pageTwo')
621    .onBackPressed(() => {
622      this.pageInfo.pop();
623      return true;
624    })
625  }
626}
627```
628
629For API version 17 or earlier:
630
631Click **Next page** to enter the next page and then return to the previous page. All labels of **Tabcontent** are unfrozen.
632
633For API version 18 or later:
634
635Click **Next page** to enter the next page and then return to the previous page. Only the nodes with the corresponding labels are unfrozen.
636
637## Constraints
638
639As 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.
640
641```
642import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
643
644// Define a Params class to pass parameters.
645@ObservedV2
646class Params {
647  // Singleton mode. Ensure that there is only one Params instance.
648  static singleton_: Params;
649
650  // Method for obtaining the Params instance.
651  static instance() {
652    if (!Params.singleton_) {
653      Params.singleton_ = new Params(0);
654    }
655    return Params.singleton_;
656  }
657
658  // Use the @Trace decorator to decorate the message attribute so that its changes are observable.
659  @Trace message: string = "Hello";
660  index: number = 0;
661
662  constructor(index: number) {
663    this.index = index;
664  }
665}
666
667// Define a buildNodeChild component that contains a message attribute and an index attribute.
668@ComponentV2
669struct buildNodeChild {
670  // Use the Params instance as the storage attribute.
671  storage: Params = Params.instance();
672  @Param index: number = 0;
673
674  // Use the @Monitor decorator to listen for the changes of storage.message.
675  @Monitor("storage.message")
676  onMessageChange(monitor: IMonitor) {
677    console.log(`FreezeBuildNode buildNodeChild message callback func ${this.storage.message}, index:${this.index}`);
678  }
679
680  build() {
681    Text(`buildNode Child message: ${this.storage.message}`).fontSize(30)
682  }
683}
684
685// Define a buildText function that receives a Params parameter and constructs a Column component.
686@Builder
687function buildText(params: Params) {
688  Column() {
689    buildNodeChild({ index: params.index })
690  }
691}
692
693class TextNodeController extends NodeController {
694  private textNode: BuilderNode<[Params]> | null = null;
695  private index: number = 0;
696
697  // The constructor receives an index parameter.
698  constructor(index: number) {
699    super();
700    this.index = index;
701  }
702
703  // Create and return a FrameNode.
704  makeNode(context: UIContext): FrameNode | null {
705    this.textNode = new BuilderNode(context);
706    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index));
707    return this.textNode.getFrameNode();
708  }
709}
710
711// Define an index component that contains a message attribute and a data array.
712@Entry
713@ComponentV2
714struct Index {
715  // Use the Params instance as the storage attribute.
716  storage: Params = Params.instance();
717  private data: number[] = [0, 1];
718
719  build() {
720    Row() {
721      Column() {
722        Button("change").fontSize(30)
723          .onClick(() => {
724            this.storage.message += 'a';
725          })
726
727        Tabs() {
728          // Use Repeat to repeatedly render the TabContent component.
729          Repeat<number>(this.data)
730            .each((obj: RepeatItem<number>) => {
731              TabContent() {
732                FreezeBuildNode({ index: obj.item })
733                  .margin({ top: 20 })
734              }.tabBar(`tab${obj.item}`)
735            })
736            .key((item: number) => item.toString())
737        }
738      }
739    }
740    .width('100%')
741    .height('100%')
742  }
743}
744
745// Define a FreezeBuildNode component that contains a message attribute and an index attribute.
746@ComponentV2({ freezeWhenInactive: true })
747struct FreezeBuildNode {
748  // Use the Params instance as the storage attribute.
749  storage: Params = Params.instance();
750  @Param index: number = 0;
751
752  // Use the @Monitor decorator to listen for the changes of storage.message.
753  @Monitor("storage.message")
754  onMessageChange(monitor: IMonitor) {
755    console.log(`FreezeBuildNode message callback func ${this.storage.message}, index: ${this.index}`);
756  }
757
758  build() {
759    NodeContainer(new TextNodeController(this.index))
760      .width('100%')
761      .height('100%')
762      .backgroundColor('#FFF0F0F0')
763  }
764}
765```
766
767Click **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.
768
769![builderNode.gif](figures/builderNode.gif)
770
771<!--no_check-->