Freezing a Custom Component
When 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.
Before reading this topic, you are advised to read @ComponentV2.
NOTE
@ComponentV2 decorated custom component freezing is supported since API version 12.
Different from freezing the @Component decorated components, custom components decorated by @ComponentV2 do not support freezing the cached list items in the LazyForEach scenario.
Use Scenarios
Page Routing
NOTE
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.
- 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. For details, see the following.

Page 1
import { router } from '@kit.ArkUI';
@ObservedV2
export class Book {
@Trace name: string = "100";
constructor(page: string) {
this.name = page;
}
}
@Entry
@ComponentV2({ freezeWhenInactive: true })
export struct Page1 {
@Local bookTest: Book = new Book("A Midsummer Night's Dream");
@Monitor("bookTest.name")
onMessageChange(monitor: IMonitor) {
console.log(`The book name change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
}
build() {
Column() {
Text(`Book name is ${this.bookTest.name}`).fontSize(25)
Button('changeBookName').fontSize(25)
.onClick(() => {
this.bookTest.name = "The Old Man and the Sea";
})
Button('go to next page').fontSize(25)
.onClick(() => {
router.pushUrl({ url: 'pages/Page2' });
setTimeout(() => {
this.bookTest = new Book("Jane Austen oPride and Prejudice");
}, 1000)
})
}
}
}
Page 2
import { router } from '@kit.ArkUI';
@Entry
@ComponentV2
struct Page2 {
build() {
Column() {
Text(`This is the page2`).fontSize(25)
Button('Back')
.onClick(() => {
router.back();
})
}
}
}
In the preceding example:
-
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.
-
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. The trace diagram is as follows.

- 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.

TabContent
-
You can freeze invisible TabContent components in the Tabs container so that they do not trigger UI re-rendering.
-
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.
For details, see the following.

@Entry
@ComponentV2
struct TabContentTest {
@Local message: number = 0;
@Local data: number[] = [0, 1];
build() {
Row() {
Column() {
Button('change message').onClick(() => {
this.message++;
})
Tabs() {
ForEach(this.data, (item: number) => {
TabContent() {
FreezeChild({ message: this.message, index: item })
}.tabBar(`tab${item}`)
}, (item: number) => item.toString())
}
}
.width('100%')
}
.height('100%')
}
}
@ComponentV2({ freezeWhenInactive: true })
struct FreezeChild {
@Param message: number = 0;
@Param index: number = 0;
@Monitor('message') onMessageUpdated(mon: IMonitor) {
console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`);
}
build() {
Text("message" + `${this.message}, index: ${this.index}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
In the preceding example:
-
When change message is clicked, the value of message changes, and the @Monitor decorated onMessageUpdated method of the TabContent component being displayed is called.
-
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.
-
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.

Navigation
- 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.
@Entry
@ComponentV2
struct MyNavigationTestStack {
@Provider('pageInfo') pageInfo: NavPathStack = new NavPathStack();
@Local message: number = 0;
@Monitor('message') info() {
console.info(`freeze-test MyNavigation message callback ${this.message}`);
}
@Builder
PageMap(name: string) {
if (name === 'pageOne') {
pageOneStack({ message: this.message })
} else if (name === 'pageTwo') {
pageTwoStack({ message: this.message })
} else if (name === 'pageThree') {
pageThreeStack({ message: this.message })
}
}
build() {
Column() {
Button('change message')
.onClick(() => {
this.message++;
})
Navigation(this.pageInfo) {
Column() {
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.onClick(() => {
this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack.
})
}
}.title('NavIndex')
.navDestination(this.PageMap)
.mode(NavigationMode.Stack)
}
}
}
@ComponentV2
struct pageOneStack {
@Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
@Local index: number = 1;
@Param message: number = 0;
build() {
NavDestination() {
Column() {
NavigationContentMsgStack({ message: this.message, index: this.index })
Text("cur stack size:" + `${this.pageInfo.size()}`)
.fontSize(30)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.onClick(() => {
this.pageInfo.pushPathByName('pageTwo', null);
})
Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
.onClick(() => {
this.pageInfo.pop();
})
}.width('100%').height('100%')
}.title('pageOne')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}
@ComponentV2
struct pageTwoStack {
@Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
@Local index: number = 2;
@Param message: number = 0;
build() {
NavDestination() {
Column() {
NavigationContentMsgStack({ message: this.message, index: this.index })
Text("cur stack size:" + `${this.pageInfo.size()}`)
.fontSize(30)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.onClick(() => {
this.pageInfo.pushPathByName('pageThree', null);
})
Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
.onClick(() => {
this.pageInfo.pop();
})
}
}.title('pageTwo')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}
@ComponentV2
struct pageThreeStack {
@Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
@Local index: number = 3;
@Param message: number = 0;
build() {
NavDestination() {
Column() {
NavigationContentMsgStack({ message: this.message, index: this.index })
Text("cur stack size:" + `${this.pageInfo.size()}`)
.fontSize(30)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.height(40)
.onClick(() => {
this.pageInfo.pushPathByName('pageOne', null);
})
Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
.height(40)
.onClick(() => {
this.pageInfo.pop();
})
}
}.title('pageThree')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}
@ComponentV2({ freezeWhenInactive: true })
struct NavigationContentMsgStack {
@Param message: number = 0;
@Param index: number = 0;
@Monitor('message') info() {
console.info(`freeze-test NavigationContent message callback ${this.message}`);
console.info(`freeze-test ---- called by content ${this.index}`);
}
build() {
Column() {
Text("msg:" + `${this.message}`)
.fontSize(30)
}
}
}
In the preceding example:
-
When change message is clicked, the value of message changes, and the @Monitor decorated info method of the MyNavigationTestStack component being displayed is called.
-
When Next Page is clicked, the page is switched to PageOne and the pageOneStack node is created.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
When Back Page is clicked, the page is switched to the initial page.

Constraints
As shown in the following example, the custom node BuilderNode 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.
import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
// Define a Params class to pass parameters.
@ObservedV2
class Params {
// Singleton mode. Ensure that there is only one Params instance.
static singleton_: Params;
// Method for obtaining the Params instance.
static instance() {
if (!Params.singleton_) {
Params.singleton_ = new Params(0);
}
return Params.singleton_;
}
// Use the @Trace decorator to decorate the message attribute so that its changes are observable.
@Trace message: string = "Hello";
index: number = 0;
constructor(index: number) {
this.index = index;
}
}
// Define a buildNodeChild component that contains a message attribute and an index attribute.
@ComponentV2
struct buildNodeChild {
// Use the Params instance as the storage attribute.
storage: Params = Params.instance();
@Param index: number = 0;
// Use the @Monitor decorator to listen for the changes of storage.message.
@Monitor("storage.message")
onMessageChange(monitor: IMonitor) {
console.log(`FreezeBuildNode buildNodeChild message callback func ${this.storage.message}, index:${this.index}`);
}
build() {
Text(`buildNode Child message: ${this.storage.message}`).fontSize(30)
}
}
// Define a buildText function that receives a Params parameter and constructs a Column component.
@Builder
function buildText(params: Params) {
Column() {
buildNodeChild({ index: params.index })
}
}
class TextNodeController extends NodeController {
private textNode: BuilderNode<[Params]> | null = null;
private index: number = 0;
// The constructor receives an index parameter.
constructor(index: number) {
super();
this.index = index;
}
// Create and return a FrameNode.
makeNode(context: UIContext): FrameNode | null {
this.textNode = new BuilderNode(context);
this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index));
return this.textNode.getFrameNode();
}
}
// Define an index component that contains a message attribute and a data array.
@Entry
@ComponentV2
struct Index {
// Use the Params instance as the storage attribute.
storage: Params = Params.instance();
private data: number[] = [0, 1];
build() {
Row() {
Column() {
Button("change").fontSize(30)
.onClick(() => {
this.storage.message += 'a';
})
Tabs() {
// Use Repeat to repeatedly render the TabContent component.
Repeat<number>(this.data)
.each((obj: RepeatItem<number>) => {
TabContent() {
FreezeBuildNode({ index: obj.item })
.margin({ top: 20 })
}.tabBar(`tab${obj.item}`)
})
.key((item: number) => item.toString())
}
}
}
.width('100%')
.height('100%')
}
}
// Define a FreezeBuildNode component that contains a message attribute and an index attribute.
@ComponentV2({ freezeWhenInactive: true })
struct FreezeBuildNode {
// Use the Params instance as the storage attribute.
storage: Params = Params.instance();
@Param index: number = 0;
// Use the @Monitor decorator to listen for the changes of storage.message.
@Monitor("storage.message")
onMessageChange(monitor: IMonitor) {
console.log(`FreezeBuildNode message callback func ${this.storage.message}, index: ${this.index}`);
}
build() {
NodeContainer(new TextNodeController(this.index))
.width('100%')
.height('100%')
.backgroundColor('#FFF0F0F0')
}
}
Click 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.
