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> @ComponentV2 decorated custom component freezing is supported since API version 12.
10>
11> Different from freezing the @Component decorated components, custom components decorated by @ComponentV2 do not support freezing the cached list items in the **LazyForEach** scenario.
12
13
14## Use Scenarios
15
16### Page Routing
17
18> **NOTE**
19>
20> 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).
21
22- 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.
23For details, see the following.
24
25![freezeInPage](./figures/freezeInPage.png)
26
27Page 1
28
29```ts
30import { router } from '@kit.ArkUI';
31
32@ObservedV2
33export class Book {
34  @Trace name: string = "100";
35
36  constructor(page: string) {
37    this.name = page;
38  }
39}
40
41@Entry
42@ComponentV2({ freezeWhenInactive: true })
43export struct Page1 {
44  @Local bookTest: Book = new Book("A Midsummer Night's Dream");
45
46  @Monitor("bookTest.name")
47  onMessageChange(monitor: IMonitor) {
48    console.log(`The book name change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
49  }
50
51  build() {
52    Column() {
53      Text(`Book name is ${this.bookTest.name}`).fontSize(25)
54      Button('changeBookName').fontSize(25)
55        .onClick(() => {
56          this.bookTest.name = "The Old Man and the Sea";
57        })
58      Button('go to next page').fontSize(25)
59        .onClick(() => {
60          router.pushUrl({ url: 'pages/Page2' });
61          setTimeout(() => {
62            this.bookTest = new Book("Jane Austen oPride and Prejudice");
63          }, 1000)
64        })
65    }
66  }
67}
68```
69
70Page 2
71
72```ts
73import { router } from '@kit.ArkUI';
74
75@Entry
76@ComponentV2
77struct Page2 {
78  build() {
79    Column() {
80      Text(`This is the page2`).fontSize(25)
81      Button('Back')
82        .onClick(() => {
83          router.back();
84        })
85    }
86  }
87}
88```
89
90In the preceding example:
91
921. 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.
93
942. 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.
95The trace diagram is as follows.
96
97![Example Image](./figures/freeze1.png)
98
99
1003. 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.
101
102![freezeV2Page](./figures/freezeV2page.gif)
103
104
105### TabContent
106
107- You can freeze invisible **TabContent** components in the **Tabs** container so that they do not trigger UI re-rendering.
108
109- 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.
110
111For details, see the following.
112![freezeWithTab](./figures/freezewithTabs.png)
113
114
115```ts
116@Entry
117@ComponentV2
118struct TabContentTest {
119  @Local message: number = 0;
120  @Local data: number[] = [0, 1];
121
122  build() {
123    Row() {
124      Column() {
125        Button('change message').onClick(() => {
126          this.message++;
127        })
128
129        Tabs() {
130          ForEach(this.data, (item: number) => {
131            TabContent() {
132              FreezeChild({ message: this.message, index: item })
133            }.tabBar(`tab${item}`)
134          }, (item: number) => item.toString())
135        }
136      }
137      .width('100%')
138    }
139    .height('100%')
140  }
141}
142
143@ComponentV2({ freezeWhenInactive: true })
144struct FreezeChild {
145  @Param message: number = 0;
146  @Param index: number = 0;
147
148  @Monitor('message') onMessageUpdated(mon: IMonitor) {
149    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`);
150  }
151
152  build() {
153    Text("message" + `${this.message}, index: ${this.index}`)
154      .fontSize(50)
155      .fontWeight(FontWeight.Bold)
156  }
157}
158```
159
160In the preceding example:
161
1621. When **change message** is clicked, the value of **message** changes, and the @Monitor decorated **onMessageUpdated** method of the **TabContent** component being displayed is called.
163
1642. 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.
165
1663. 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.
167
168![TabContent.gif](figures/TabContent.gif)
169
170
171### Navigation
172
173- You can freeze an invisible page so that it does not trigger UI re-rendering. When the user returns to this page, a re-render is triggered through an @Monitor decorated callback.
174
175```ts
176@Entry
177@ComponentV2
178struct MyNavigationTestStack {
179  @Provider('pageInfo') pageInfo: NavPathStack = new NavPathStack();
180  @Local message: number = 0;
181
182  @Monitor('message') info() {
183    console.info(`freeze-test MyNavigation message callback ${this.message}`);
184  }
185
186  @Builder
187  PageMap(name: string) {
188    if (name === 'pageOne') {
189      pageOneStack({ message: this.message })
190    } else if (name === 'pageTwo') {
191      pageTwoStack({ message: this.message })
192    } else if (name === 'pageThree') {
193      pageThreeStack({ message: this.message })
194    }
195  }
196
197  build() {
198    Column() {
199      Button('change message')
200        .onClick(() => {
201          this.message++;
202        })
203      Navigation(this.pageInfo) {
204        Column() {
205          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
206            .onClick(() => {
207              this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack.
208            })
209        }
210      }.title('NavIndex')
211      .navDestination(this.PageMap)
212      .mode(NavigationMode.Stack)
213    }
214  }
215}
216
217@ComponentV2
218struct pageOneStack {
219  @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
220  @Local index: number = 1;
221  @Param message: number = 0;
222
223  build() {
224    NavDestination() {
225      Column() {
226        NavigationContentMsgStack({ message: this.message, index: this.index })
227        Text("cur stack size:" + `${this.pageInfo.size()}`)
228          .fontSize(30)
229        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
230          .onClick(() => {
231            this.pageInfo.pushPathByName('pageTwo', null);
232          })
233        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
234          .onClick(() => {
235            this.pageInfo.pop();
236          })
237      }.width('100%').height('100%')
238    }.title('pageOne')
239    .onBackPressed(() => {
240      this.pageInfo.pop();
241      return true;
242    })
243  }
244}
245
246@ComponentV2
247struct pageTwoStack {
248  @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
249  @Local index: number = 2;
250  @Param message: number = 0;
251
252  build() {
253    NavDestination() {
254      Column() {
255        NavigationContentMsgStack({ message: this.message, index: this.index })
256        Text("cur stack size:" + `${this.pageInfo.size()}`)
257          .fontSize(30)
258        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
259          .onClick(() => {
260            this.pageInfo.pushPathByName('pageThree', null);
261          })
262        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
263          .onClick(() => {
264            this.pageInfo.pop();
265          })
266      }
267    }.title('pageTwo')
268    .onBackPressed(() => {
269      this.pageInfo.pop();
270      return true;
271    })
272  }
273}
274
275@ComponentV2
276struct pageThreeStack {
277  @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
278  @Local index: number = 3;
279  @Param message: number = 0;
280
281  build() {
282    NavDestination() {
283      Column() {
284        NavigationContentMsgStack({ message: this.message, index: this.index })
285        Text("cur stack size:" + `${this.pageInfo.size()}`)
286          .fontSize(30)
287        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
288          .height(40)
289          .onClick(() => {
290            this.pageInfo.pushPathByName('pageOne', null);
291          })
292        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
293          .height(40)
294          .onClick(() => {
295            this.pageInfo.pop();
296          })
297      }
298    }.title('pageThree')
299    .onBackPressed(() => {
300      this.pageInfo.pop();
301      return true;
302    })
303  }
304}
305
306@ComponentV2({ freezeWhenInactive: true })
307struct NavigationContentMsgStack {
308  @Param message: number = 0;
309  @Param index: number = 0;
310
311  @Monitor('message') info() {
312    console.info(`freeze-test NavigationContent message callback ${this.message}`);
313    console.info(`freeze-test ---- called by content ${this.index}`);
314  }
315
316  build() {
317    Column() {
318      Text("msg:" + `${this.message}`)
319        .fontSize(30)
320    }
321  }
322}
323```
324
325In the preceding example:
326
3271. When **change message** is clicked, the value of **message** changes, and the @Monitor decorated **info** method of the **MyNavigationTestStack** component being displayed is called.
328
3292. When **Next Page** is clicked, the page is switched to **PageOne** and the **pageOneStack** node is created.
330
3313. 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.
332
3334. 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.
334
3355. 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.
336
3376. 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.
338
3397. 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.
340
3418. 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.
342
3439. 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.
344
34510. When **Back Page** is clicked, the page is switched to the initial page.
346
347![navigation-freeze.gif](figures/navigation-freeze.gif)
348
349## Constraints
350As 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.
351
352```
353import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
354
355// Define a Params class to pass parameters.
356@ObservedV2
357class Params {
358  // Singleton mode. Ensure that there is only one Params instance.
359  static singleton_: Params;
360
361  // Method for obtaining the Params instance.
362  static instance() {
363    if (!Params.singleton_) {
364      Params.singleton_ = new Params(0);
365    }
366    return Params.singleton_;
367  }
368
369  // Use the @Trace decorator to decorate the message attribute so that its changes are observable.
370  @Trace message: string = "Hello";
371  index: number = 0;
372
373  constructor(index: number) {
374    this.index = index;
375  }
376}
377
378// Define a buildNodeChild component that contains a message attribute and an index attribute.
379@ComponentV2
380struct buildNodeChild {
381  // Use the Params instance as the storage attribute.
382  storage: Params = Params.instance();
383  @Param index: number = 0;
384
385  // Use the @Monitor decorator to listen for the changes of storage.message.
386  @Monitor("storage.message")
387  onMessageChange(monitor: IMonitor) {
388    console.log(`FreezeBuildNode buildNodeChild message callback func ${this.storage.message}, index:${this.index}`);
389  }
390
391  build() {
392    Text(`buildNode Child message: ${this.storage.message}`).fontSize(30)
393  }
394}
395
396// Define a buildText function that receives a Params parameter and constructs a Column component.
397@Builder
398function buildText(params: Params) {
399  Column() {
400    buildNodeChild({ index: params.index })
401  }
402}
403
404class TextNodeController extends NodeController {
405  private textNode: BuilderNode<[Params]> | null = null;
406  private index: number = 0;
407
408  // The constructor receives an index parameter.
409  constructor(index: number) {
410    super();
411    this.index = index;
412  }
413
414  // Create and return a FrameNode.
415  makeNode(context: UIContext): FrameNode | null {
416    this.textNode = new BuilderNode(context);
417    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index));
418    return this.textNode.getFrameNode();
419  }
420}
421
422// Define an index component that contains a message attribute and a data array.
423@Entry
424@ComponentV2
425struct Index {
426  // Use the Params instance as the storage attribute.
427  storage: Params = Params.instance();
428  private data: number[] = [0, 1];
429
430  build() {
431    Row() {
432      Column() {
433        Button("change").fontSize(30)
434          .onClick(() => {
435            this.storage.message += 'a';
436          })
437
438        Tabs() {
439          // Use Repeat to repeatedly render the TabContent component.
440          Repeat<number>(this.data)
441            .each((obj: RepeatItem<number>) => {
442              TabContent() {
443                FreezeBuildNode({ index: obj.item })
444                  .margin({ top: 20 })
445              }.tabBar(`tab${obj.item}`)
446            })
447            .key((item: number) => item.toString())
448        }
449      }
450    }
451    .width('100%')
452    .height('100%')
453  }
454}
455
456// Define a FreezeBuildNode component that contains a message attribute and an index attribute.
457@ComponentV2({ freezeWhenInactive: true })
458struct FreezeBuildNode {
459  // Use the Params instance as the storage attribute.
460  storage: Params = Params.instance();
461  @Param index: number = 0;
462
463  // Use the @Monitor decorator to listen for the changes of storage.message.
464  @Monitor("storage.message")
465  onMessageChange(monitor: IMonitor) {
466    console.log(`FreezeBuildNode message callback func ${this.storage.message}, index: ${this.index}`);
467  }
468
469  build() {
470    NodeContainer(new TextNodeController(this.index))
471      .width('100%')
472      .height('100%')
473      .backgroundColor('#FFF0F0F0')
474  }
475}
476```
477
478Click **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.
479
480![builderNode.gif](figures/builderNode.gif)
481