@Reusable Decorator: Reusing V1 Components

Custom components decorated with @Reusable support component reuse. When a custom component is removed from the component tree, it is cached in the cache pool. When new components of the same type are needed, the system reuses cached objects instead of creating new ones. This avoids repeated creation and destruction of components and improves performance.

NOTE

The @Reusable decorator is supported since API version 10 and can be used in ArkTS.

For details about the principles, optimization methods, and use scenarios of component reuse, see Component Reuse.

Overview

Using @Reusable to decorate the custom components indicates that these components are reusable.

When developing complex UIs, rendering efficiency is a key consideration. For example, during rapid scrolling through long lists, the frequent creation and destruction of numerous list items can cause interface stuttering. Component reuse is an important method for optimizing UI performance. By reusing previously created component objects that have been detached from the tree, the frequency of component creation and destruction is reduced, thereby lowering computational overhead and improving UI rendering efficiency.

NOTE

  • Custom components decorated with @Reusable (including view nodes, component instances, and state context), when removed from the component tree, will be placed into their parent custom component's cache pool. Subsequent component creation will prioritize reusing nodes from this cache pool, saving the time required for component re-creation.
  • @Reusable provides two lifecycle callbacks: aboutToRecycle and aboutToReuse. aboutToRecycle is called to recycle a component, and aboutToReuse is called to reuse a component. You can implement the service logic related to component recycling and reuse in these two lifecycle callbacks.
  • When a @Reusable-decorated custom component has child components, the child components' aboutToRecycle and aboutToReuse will be called recursively during recycling and reuse (regardless of whether the child components are marked with @Reusable), continuing until all child components are traversed.
  • The component structure should remain unchanged before and after reuse. For scenarios where component structures differ, reuseId can be used to distinguish between reusable components with different structures.
  • If a custom component decorated with @Reusable has no child components, recycling and reuse will not be triggered.

Constraints

Custom Components Only

The @Reusable decorator is only applicable to custom components @Component and cannot be used together with @Builder.

@Reusable is not compatible with @ComponentV2. For @ComponentV2 reuse, use the @ReusableV2decorator instead.

import { ComponentContent } from '@kit.ArkUI';

// @Builder cannot be used together with @Reusable.
// @Reusable
@Builder
function buildCreativeLoadingDialog(closedClick: () => void) {
  Crash();
}

@Component
export struct Crash {
  build() {
    Column() {
      Text('Crash')
        .fontSize(12)
        .lineHeight(18)
        .fontColor(Color.Blue)
        .margin({
          left: 6
        })
    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  private uiContext = this.getUIContext();

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('Index')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => {
          });
          this.uiContext.getPromptAction().openCustomDialog(contentNode);
        })
    }
    .height('100%')
    .width('100%')
  }
}

Constraints on Updating State Variables

When a custom component decorated with @Reusable is reused, the aboutToReuse API is invoked recursively for the component and all its child components. Avoid modifying state variables of the parent component in its child component's aboutToReuse API. Such modification will not take effect. To update the parent component's state variables, use setTimeout to delay execution, moving the task outside the scope of component reuse.

Incorrect Usage

Modifying a parent component's state variable in its child component's aboutToReuse API:

class IncorrectBasicDataSource implements IDataSource {
  private listener: DataChangeListener | undefined = undefined;
  public dataArray: number[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): number {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct IncorrectIndex {
  private data: IncorrectBasicDataSource = new IncorrectBasicDataSource();

  aboutToAppear(): void {
    for (let index = 1; index < 20; index++) {
      this.data.dataArray.push(index);
    }
  }

  build() {
    List() {
      LazyForEach(this.data, (item: number, index: number) => {
        ListItem() {
          IncorrectReuseComponent({ num: item });
        }
      }, (item: number, index: number) => index.toString())
    }.cachedCount(0)
  }
}

@Reusable
@Component
struct IncorrectReuseComponent {
  @State num: number = 0;

  aboutToReuse(params: ESObject): void {
    this.num = params.num;
  }

  build() {
    Column() {
      Text('ReuseComponent num:' + this.num.toString())
      IncorrectReuseComponentChild({ num: this.num })
      Button('plus')
        .onClick(() => {
          this.num += 10;
        })
    }
    .height(200)
  }
}

@Component
struct IncorrectReuseComponentChild {
  @Link num: number;

  aboutToReuse(params: ESObject): void {
    this.num = -1 * params.num;
  }

  build() {
    Text('ReuseComponentChild num:' + this.num.toString())
  }
}

Correct Usage

To modify a parent component's state variable in a child component's aboutToReuse API, use setTimeout to move the modification outside the scope of component reuse:

class BasicDataSource implements IDataSource {
  private listener: DataChangeListener | undefined = undefined;
  public dataArray: number[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): number {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct Index {
  private data: BasicDataSource = new BasicDataSource();

  aboutToAppear(): void {
    for (let index = 1; index <= 20; index++) { // Loop 20 times.
      this.data.dataArray.push(index);
    }
  }

  build() {
    List() {
      LazyForEach(this.data, (item: number, index: number) => {
        ListItem() {
          ReuseComponent({ num: item })
        }
      }, (item: number, index: number) => index.toString())
    }.cachedCount(0)
  }
}

@Reusable
@Component
struct ReuseComponent {
  @State num: number = 0;

  aboutToReuse(params: ESObject): void {
    this.num = params.num;
  }

  build() {
    Column() {
      Text('ReuseComponent num:' + this.num.toString())
      ReuseComponentChild({ num: this.num })
      Button('plus')
        .onClick(() => {
          this.num += 10; // Increment the number by 10 on each click.
        })
    }
    .height(200)
  }
}

@Component
struct ReuseComponentChild {
  @Link num: number;

  aboutToReuse(params: ESObject): void {
    setTimeout(() => {
      this.num = -1 * params.num;
    }, 1)
  }

  build() {
    Text('ReuseComponentChild num:' + this.num.toString());
  }
}

Consistent Component Structure

Custom components decorated with @Reusable should maintain the same component structure before and after reuse. Otherwise, child components may be created or destroyed during the reuse process, reducing reuse efficiency and performance, and potentially causing abnormal application behavior.
For child components created during reuse, the framework will call the aboutToReuse method followed by the aboutToAppear method after their creation. When aboutToReuse is called, since aboutToAppear has not yet been executed and internal child components have not been created, operations that depend on the execution result of aboutToAppear or the states of internal child components in aboutToReuse will cause unexpected behavior. After calling aboutToReuse, the framework will then call aboutToAppear to initialize the component.
For scenarios where component structures differ, you must differentiate them by setting different reuseId values. For details, see Scenarios Involving Multiple Item Types.

Incorrect Usage

Component structures differ but are not distinguished via reuseId.
In the following example, first click the show/hide branch A button to recycle the component, then click the show/hide branch B button to reuse the component. The child component ReusableChildB is created during reuse, and both the aboutToReuse and aboutToAppear methods are called sequentially.

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';

@Entry
@Component
struct Index {
  @State showBranchA: boolean = true;
  @State showBranchB: boolean = false;

  build() {
    Column({ space: 5 }) {
      Button('show/hide branch A')
        .onClick(() => {
          this.showBranchA = !this.showBranchA;
        })
      if (this.showBranchA) {
        ReusableComponent({ flag: true })
      }
      Button('show/hide branch B')
        .onClick(() => {
          this.showBranchB = !this.showBranchB;
        })
      if (this.showBranchB) {
        ReusableComponent({ flag: false })
      }
    }
  }
}

@Reusable
@Component
struct ReusableComponent {
  @Require @Prop flag: boolean = true;

  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToAppear');
  }

  aboutToReuse(params: ESObject) {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToReuse');
    this.flag = params.flag;
  }

  build() {
    Column({ space: 5 }) {
      Text('ReusableComponent')
      if (this.flag) {
        ReusableChildA()
      } else {
        ReusableChildB()
      }
    }.border({ width: 1 })
  }
}

@Component
struct ReusableChildA {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToAppear');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToReuse');
  }

  build() {
    Text('ReusableChildA')
      .border({ width: 1 })
  }
}

@Component
struct ReusableChildB {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToAppear');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToReuse');
  }

  build() {
    Text('ReusableChildB')
      .border({ width: 1 })
  }
}

Correct Usage

Component structures differ and are distinguished via reuseId.

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';

@Entry
@Component
struct Index {
  @State showBranchA: boolean = true;
  @State showBranchB: boolean = false;

  build() {
    Column({ space: 5 }) {
      Button('show/hide branch A')
        .onClick(() => {
          this.showBranchA = !this.showBranchA;
        })
      if (this.showBranchA) {
        ReusableComponent({ flag: true })
          .reuseId('ReuseA') // Use reuseId to distinguish between reusable components with different structures.
      }
      Button('show/hide branch B')
        .onClick(() => {
          this.showBranchB = !this.showBranchB;
        })
      if (this.showBranchB) {
        ReusableComponent({ flag: false })
          .reuseId('ReuseB') // Use reuseId to distinguish between reusable components with different structures.
      }
    }
  }
}

@Reusable
@Component
struct ReusableComponent {
  @Require @Prop flag: boolean = true;

  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToAppear');
  }

  aboutToReuse(params: ESObject) {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToReuse');
    this.flag = params.flag;
  }

  build() {
    Column({ space: 5 }) {
      Text('ReusableComponent')
      if (this.flag) {
        ReusableChildA()
      } else {
        ReusableChildB()
      }
    }.border({ width: 1 })
  }
}

@Component
struct ReusableChildA {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToAppear');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToReuse');
  }

  build() {
    Text('ReusableChildA')
      .border({ width: 1 })
  }
}

@Component
struct ReusableChildB {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToAppear');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToReuse');
  }

  build() {
    Text('ReusableChildB')
      .border({ width: 1 })
  }
}

Unsupported ComponentContent

ComponentContent does not support passing @Reusable decorated custom components.

import { ComponentContent } from '@kit.ArkUI';

@Builder
function buildCreativeLoadingDialog(closedClick: () => void) {
  Crash();
}

// The dialog box pops up correctly if @Reusable is commented out; it crashes when @Reusable is added.
@Reusable
@Component
export struct Crash {
  build() {
    Column() {
      Text('Crash')
        .fontSize(12)
        .lineHeight(18)
        .fontColor(Color.Blue)
        .margin({
          left: 6
        })
    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  private uiContext = this.getUIContext();

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('Index')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          // ComponentContent is based on BuilderNode, which does not support @Reusable decorated custom components.
          let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => {
          });
          this.uiContext.getPromptAction().openCustomDialog(contentNode);
        })
    }
    .height('100%')
    .width('100%')
  }
}

Avoid Nesting Usage

Nesting @Reusable decorators is not recommended, as it increases memory usage, reduces reuse efficiency, and complicates maintenance. Nested usage creates additional cache pools with identical tree structures, leading to low reuse efficiency. In addition, it complicates lifecycle management and makes resource and variable sharing difficult.

Use Scenarios

Dynamic Layout Update

Repeatedly creating and removing views can trigger frequent layout calculations, which may affect frame rates. Component reuse avoids unnecessary view creation and layout recalculations, improving performance.

In the following example, the Child custom component is marked as reusable. Clicking the button updates Child, triggering reuse.

// xxx.ets
export class Message {
  public value: string | undefined;

  constructor(value: string) {
    this.value = value;
  }
}

@Entry
@Component
struct Index {
  @State switch: boolean = true;

  build() {
    Column() {
      Button('Hello')
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .onClick(() => {
          this.switch = !this.switch;
        })
      if (this.switch) {
        // If only one reusable component is used, reuseId is optional.
        Child({ message: new Message('Child') })
          .reuseId('Child');
      }
    }
    .height('100%')
    .width('100%')
  }
}

@Reusable
@Component
struct Child {
  @State message: Message = new Message('AboutToReuse');

  aboutToReuse(params: Record<string, ESObject>) {
    this.message = params.message as Message;
  }

  build() {
    Column() {
      Text(this.message.value)
        .fontSize(30)
    }
    .borderWidth(1)
    .height(100)
  }
}

List Scrolling with LazyForEach

When a user scrolls a list containing a large amount of data, frequent creation and destruction of list items can cause lag and performance issues. The reuse mechanism of the List component can reuse the existing list items to improve the scrolling smoothness.

In the following example, the CardView custom component is marked as reusable. Scrolling the list up or down triggers reuse of CardView.

class MyDataSource implements IDataSource {
  private dataArray: string[] = [];
  private listener: DataChangeListener | undefined;

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
  }

  public reloadListener(): void {
    this.listener?.onDataReloaded();
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  public unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct ReuseDemo {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 1; i <= 1000; i++) { // Loop 1000 times.
      this.data.pushData(i + '');
    }
  }

  build() {
    Column() {
      List() {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            CardView({ item: item });
          }
        }, (item: string) => item)
      }
    }
  }
}

// Reusable component
@Reusable
@Component
export struct CardView {
  // Only the item variable decorated with@State will be updated.
  @State item: string = '';

  aboutToReuse(params: Record<string, Object>): void {
    this.item = params.item as string;
  }

  build() {
    Column() {
      Text(this.item)
        .fontSize(30)
    }
    .borderWidth(1)
    .height(100)
  }
}

List Scrolling with if Statements

In the following example, the OneMoment custom component is marked as reusable. Scrolling the list up or down triggers reuse of OneMoment. reuseId can be used to assign a reuse group for reusable components. Components with the same reuseId are reused within the same reuse group. A single reusable component does not require reuseId. Using reuseId to identify reusable components avoids repeated deletion and re-creation logic in if statements, improving reuse efficiency and performance.

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';

@Entry
@Component
struct Index {
  private dataSource = new MyDataSource<FriendMoment>();

  aboutToAppear(): void {
    for (let i = 0; i < 20; i++) { // Loop 20 times.
      let title = i + 1 + 'test_if';
      // Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a blank image.
      this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon'));
    }

    for (let i = 0; i < 50; i++) { // Loop 50 times.
      let title = i + 1 + 'test_if';
      this.dataSource.pushData(new FriendMoment(i.toString(), title, ''));
    }
  }

  build() {
    Column() {
      // TopBar()
      List({ space: 3 }) {
        LazyForEach(this.dataSource, (moment: FriendMoment) => {
          ListItem() {
            // Use reuseId to control component reuse.
            OneMoment({ moment: moment })
              .reuseId((moment.image !== '') ? 'withImage' : 'noImage');
          }
        }, (moment: FriendMoment) => moment.id)
      }
      .cachedCount(0)
    }
  }
}

class FriendMoment {
  public id: string = '';
  public text: string = '';
  public title: string = '';
  public image: string = '';
  public answers: Array<ResourceStr> = [];

  constructor(id: string, title: string, image: string) {
    this.text = id;
    this.title = title;
    this.image = image;
  }
}

@Reusable
@Component
export struct OneMoment {
  @Prop moment: FriendMoment;

  // Only components with the same reuseId trigger reuse.
  aboutToReuse(params: ESObject): void {
    hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====OneMoment==reused==' + this.moment.text);
  }

  build() {
    Column() {
      Text(this.moment.text)
      // Conditional rendering with if
      if (this.moment.image !== '') {
        Flex({ wrap: FlexWrap.Wrap }) {
          Image($r(this.moment.image)).height(50).width(50);
          Image($r(this.moment.image)).height(50).width(50);
          Image($r(this.moment.image)).height(50).width(50);
          Image($r(this.moment.image)).height(50).width(50);
        }
      }
    }
  }
}

class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: T[] = [];

  public totalCount(): number {
    return 0;
  }

  public getData(index: number): T {
    return this.originDataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    });
  }
}

export class MyDataSource<T> extends BasicDataSource<T> {
  private dataArray: T[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): T {
    return this.dataArray[index];
  }

  public pushData(data: T): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

List Scrolling with ForEach

When the ForEach rendering control syntax is used to create reusable custom components, the full-expansion behavior of ForEach prevents component reuse. In the example: Clicking update successfully refreshes data, but ListItemView cannot be reused during list scrolling; clicking clear and then update allows ListItemView to be reused, as this triggers re-creation of multiple destroyed custom components within a single frame.

// xxx.ets
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';

class MyDataSource implements IDataSource {
  private dataArray: string[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
  }

  public unregisterDataChangeListener(listener: DataChangeListener): void {
  }
}

@Entry
@Component
struct Index {
  private data: MyDataSource = new MyDataSource();
  private data02: MyDataSource = new MyDataSource();
  @State isShow: boolean = true;
  @State dataSource: ListItemObject[] = [];

  aboutToAppear() {
    for (let i = 0; i < 100; i++) { // Loop 100 times.
      this.data.pushData(i.toString());
    }

    for (let i = 30; i <= 80; i++) { // Loop 50 times.
      this.data02.pushData(i.toString());
    }
  }

  build() {
    Column() {
      Row() {
        Button('clear').onClick(() => {
          for (let i = 1; i <= 50; i++) { // Loop 50 times.
            this.dataSource.pop();
          }
        }).height(40)

        Button('update').onClick(() => {
          for (let i = 1; i <= 50; i++) { // Loop 50 times.
            let obj = new ListItemObject();
            obj.id = i;
            obj.uuid = Math.random().toString();
            obj.isExpand = false;
            this.dataSource.push(obj);
          }
        }).height(40)
      }

      List({ space: 10 }) {
        ForEach(this.dataSource, (item: ListItemObject) => {
          ListItem() {
            ListItemView({
              obj: item
            })
          }
        }, (item: ListItemObject) => {
          return item.uuid.toString();
        })

      }.cachedCount(0)
      .width('100%')
      .height('100%')
    }
  }
}

@Reusable
@Component
struct ListItemView {
  @ObjectLink obj: ListItemObject;
  @State item: string = '';

  aboutToAppear(): void {
    // On first update click, scrolling fails to trigger reuse due to the full-expansion behavior of ForEach.
    hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToAppear=====ListItemView==created==' + this.item);
  }

  aboutToReuse(params: ESObject) {
    this.item = params.item;
    // Reuse succeeds after clear and update are clicked
    // (which recreates destroyed components in one frame).
    hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====ListItemView==reused==' + this.item);
  }

  build() {
    Column({ space: 10 }) {
      Text(`${this.obj.id}.Title`)
        .fontSize(16)
        .fontColor('#000000')
        .padding({
          top: 20,
          bottom: 20,
        })

      if (this.obj.isExpand) {
        Text('')
          .fontSize(14)
          .fontColor('#999999')
      }
    }
    .width('100%')
    .borderRadius(10)
    .backgroundColor(Color.White)
    .padding(15)
    .onClick(() => {
      this.obj.isExpand = !this.obj.isExpand;
    })
  }
}

@Observed
class ListItemObject {
  public uuid: string = '';
  public id: number = 0;
  public isExpand: boolean = false;
}

Grid

In the following example, the @Reusable decorator is used to decorate the custom component ReusableChildComponent in GridItem, indicating that the component can be reused.

The aboutToReuse API is triggered when the component is obtained from the reuse cache and added to the component tree during grid scrolling. This allows you to update the component's state variables to display correct content.

Note: There is no need to update state variables that automatically synchronize values (such as variables decorated with @Link, @StorageLink, @ObjectLink, or @Consume) in aboutToReuse, as this may trigger unnecessary component re-renders.

// Class MyDataSource implements the IDataSource API.
class MyDataSource implements IDataSource {
  private dataArray: number[] = [];

  public pushData(data: number): void {
    this.dataArray.push(data);
  }

  // Total number of items in the data source.
  public totalCount(): number {
    return this.dataArray.length;
  }

  // Return data at the specified index.
  public getData(index: number): number {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
  }
}

@Entry
@Component
struct MyComponent {
  // Data source.
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 1; i <= 1000; i++) { // Loop 1000 times.
      this.data.pushData(i);
    }
  }

  build() {
    Column({ space: 5 }) {
      Grid() {
        LazyForEach(this.data, (item: number) => {
          GridItem() {
            // Use the reusable custom component.
            ReusableChildComponent({ item: item });
          }
        }, (item: string) => item)
      }
      .cachedCount(2) // Set the number of cached GridItem components.
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .margin(10)
      .height(500)
      .backgroundColor(0xFAEEE0)
    }
  }
}

@Reusable
@Component
struct ReusableChildComponent {
  @State item: number = 0;

  // Called before the component is added to the component tree from the reuse cache. The component's state variable can be updated here to display the correct content.
  // The aboutToReuse parameter no longer supports the any type. Record is used to specify explicit data types, constructing an object type where property keys are strings and property values are numbers.
  aboutToReuse(params: Record<string, number>) {
    this.item = params.item;
  }

  build() {
    Column() {
      // Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error.
      Image($r('app.media.app_icon'))
        .objectFit(ImageFit.Fill)
        .layoutWeight(1)
      Text(`Image${this.item}`)
        .fontSize(16)
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height(120)
    .backgroundColor(0xF9CF93)
  }
}

WaterFlow

  • For WaterFlow scrolling scenarios where FlowItem and its child components are frequently created and destroyed, you can encapsulate components in FlowItem into a custom component and decorate it with @Reusable to implement component reuse.

    import { hilog } from '@kit.PerformanceAnalysisKit';
    
    const TAG = '[Sample_ReusableComponent]';
    const DOMAIN = 0xF811;
    const BUNDLE = 'ReusableComponent_';
    
    class WaterFlowDataSource implements IDataSource {
      private dataArray: number[] = [];
      private listeners: DataChangeListener[] = [];
    
      constructor() {
        for (let i = 0; i < 60; i++) { // Loop 60 times.
          this.dataArray.push(i);
        }
      }
    
      // Obtain data at the specified index.
      public getData(index: number): number {
        return this.dataArray[index];
      }
    
      // Notify listeners of new data addition.
      notifyDataAdd(index: number): void {
        this.listeners.forEach(listener => {
          listener.onDataAdd(index);
        });
      }
    
      // Obtain the total number of data items.
      public totalCount(): number {
        return this.dataArray.length;
      }
    
      // Register a data change listener.
      registerDataChangeListener(listener: DataChangeListener): void {
        if (this.listeners.indexOf(listener) < 0) {
          this.listeners.push(listener);
        }
      }
    
      // Unregister the data change listener.
      unregisterDataChangeListener(listener: DataChangeListener): void {
        const pos = this.listeners.indexOf(listener);
        if (pos >= 0) {
          this.listeners.splice(pos, 1);
        }
      }
    
      // Add an item to the end of the data array.
      public addLastItem(): void {
        this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
        this.notifyDataAdd(this.dataArray.length - 1);
      }
    }
    
    @Reusable
    @Component
    struct ReusableFlowItem {
      @State item: number = 0;
    
      // Called before the component is added to the component tree from the reuse cache. The component's state variable can be updated here to display the correct content.
      aboutToReuse(params: ESObject) {
        this.item = params.item;
        hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====FlowItem==reused==' + this.item);
      }
    
      aboutToRecycle(): void {
        hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToRecycle====FlowItem==recycled==' + this.item);
      }
    
      build() {
        // Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error.
        Column() {
          Text('N' + this.item).fontSize(24).height('26').margin(10);
          Image($r('app.media.app_icon'))
            .objectFit(ImageFit.Cover)
            .width(50)
            .height(50);
        }
      }
    }
    
    @Entry
    @Component
    struct Index {
      @State minSize: number = 50; // Minimum value: 50.
      @State maxSize: number = 80; // Maximum value: 80.
      @State fontSize: number = 24; // Font size: 24.
      @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F];
      scroller: Scroller = new Scroller();
      dataSource: WaterFlowDataSource = new WaterFlowDataSource();
      private itemWidthArray: number[] = [];
      private itemHeightArray: number[] = [];
    
      // Calculate random size for flow items.
      getSize() {
        let ret = Math.floor(Math.random() * this.maxSize);
        return (ret > this.minSize ? ret : this.minSize);
      }
    
      // Generate size arrays for flow items.
      getItemSizeArray() {
        for (let i = 0; i < 100; i++) { // Loop 100 times.
          this.itemWidthArray.push(this.getSize());
          this.itemHeightArray.push(this.getSize());
        }
      }
    
      aboutToAppear() {
        this.getItemSizeArray();
      }
    
      build() {
        Stack({ alignContent: Alignment.TopStart }) {
          Column({ space: 2 }) {
            Button('back top')
              .height('5%')
              .onClick(() => {
                // Scroll to top when the component is clicked.
                this.scroller.scrollEdge(Edge.Top);
              })
            WaterFlow({ scroller: this.scroller }) {
              LazyForEach(this.dataSource, (item: number) => {
                FlowItem() {
                  ReusableFlowItem({ item: item })
                }.onAppear(() => {
                  if (item + 20 == this.dataSource.totalCount()) { // Threshold: 20.
                    for (let i = 0; i < 50; i++) { // Loop 50 times.
                      this.dataSource.addLastItem();
                    }
                  }
                })
    
              })
            }
          }
        }
      }
    }
    

Swiper

  • For Swiper scrolling scenarios where child components are frequently created and destroyed, you can encapsulate the child components into a custom component and decorate it with @Reusable to implement component reuse.

    @Entry
    @Component
    struct Index {
      private dataSource = new MyDataSource<Question>();
    
      aboutToAppear(): void {
        for (let i = 0; i < 1000; i++) { // Loop 1000 times.
          let title = i + 1 + 'test_swiper';
          let answers = ['test1', 'test2', 'test3', 'test4'];
          // Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error.
          this.dataSource.pushData(new Question(i.toString(), title, $r('app.media.app_icon'), answers));
        }
      }
    
      build() {
        Column({ space: 5 }) {
          Swiper() {
            LazyForEach(this.dataSource, (item: Question) => {
              QuestionSwiperItem({ itemData: item });
            }, (item: Question) => item.id)
          }
        }
        .width('100%')
        .margin({ top: 5 })
      }
    }
    
    class Question {
      public id: string = '';
      public title: ResourceStr = '';
      public image: ResourceStr = '';
      public answers: Array<ResourceStr> = [];
    
      constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array<ResourceStr>) {
        this.id = id;
        this.title = title;
        this.image = image;
        this.answers = answers;
      }
    }
    
    @Reusable
    @Component
    struct QuestionSwiperItem {
      @State itemData: Question | null = null;
    
      aboutToReuse(params: Record<string, Object>): void {
        this.itemData = params.itemData as Question;
      }
    
      build() {
        Column() {
          Text(this.itemData?.title)
            .fontSize(18)
            .fontColor($r('sys.color.ohos_id_color_primary'))
            .alignSelf(ItemAlign.Start)
            .margin({
              top: 10,
              bottom: 16
            })
            
          Image(this.itemData?.image)
            .width('100%')
            .borderRadius(12)
            .objectFit(ImageFit.Contain)
            .margin({
              bottom: 16
            })
            .height(80)
            .width(80)
    
          Column({ space: 16 }) {
            ForEach(this.itemData?.answers, (item: Resource) => {
              Text(item)
                .fontSize(16)
                .fontColor($r('sys.color.ohos_id_color_primary'))
            }, (item: ResourceStr) => JSON.stringify(item))
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding({
          left: 16,
          right: 16
        })
      }
    }
    
    class BasicDataSource<T> implements IDataSource {
      private listeners: DataChangeListener[] = [];
      private originDataArray: T[] = [];
    
      public totalCount(): number {
        return 0;
      }
    
      public getData(index: number): T {
        return this.originDataArray[index];
      }
    
      registerDataChangeListener(listener: DataChangeListener): void {
        if (this.listeners.indexOf(listener) < 0) {
          this.listeners.push(listener);
        }
      }
    
      unregisterDataChangeListener(listener: DataChangeListener): void {
        const pos = this.listeners.indexOf(listener);
        if (pos >= 0) {
          this.listeners.splice(pos, 1);
        }
      }
    
      notifyDataAdd(index: number): void {
        this.listeners.forEach(listener => {
          listener.onDataAdd(index);
        });
      }
    }
    
    export class MyDataSource<T> extends BasicDataSource<T> {
      private dataArray: T[] = [];
    
      public totalCount(): number {
        return this.dataArray.length;
      }
    
      public getData(index: number): T {
        return this.dataArray[index];
      }
    
      public pushData(data: T): void {
        this.dataArray.push(data);
        this.notifyDataAdd(this.dataArray.length - 1);
      }
    }
    

List Scrolling with ListItemGroup

  • For list scrolling scenarios where the ListItemGroup component is used, you can encapsulate child components in ListItem that need to be destroyed and re-created into a custom component and decorate it with @Reusable to implement component reuse.

    @Entry
    @Component
    struct ListItemGroupAndReusable {
      data: DataSrc2 = new DataSrc2();
    
      @Builder
      itemHead(text: string) {
        Text(text)
          .fontSize(20)
          .backgroundColor(0xAABBCC)
          .width('100%')
          .padding(10)
      }
    
      aboutToAppear() {
        for (let i = 0; i < 10000; i++) { // Loop 10000 times.
          let data1 = new DataSrc1();
          for (let j = 0; j < 12; j++) { // Loop 12 times.
            data1.data.push(`Test item data: ${i} - ${j}`);
          }
          this.data.data.push(data1);
        }
      }
    
      build() {
        Stack() {
          List() {
            LazyForEach(this.data, (item: DataSrc1, index: number) => {
              ListItemGroup({ header: this.itemHead(index.toString()) }) {
                LazyForEach(item, (ii: string, index: number) => {
                  ListItem() {
                    Inner({ str: ii });
                  }
                })
              }
              .width('100%')
              .height('60vp')
            })
          }
        }
        .width('100%')
        .height('100%')
      }
    }
    
    @Reusable
    @Component
    struct Inner {
      @State str: string = '';
    
      aboutToReuse(param: ESObject) {
        this.str = param.str;
      }
    
      build() {
        Text(this.str);
      }
    }
    
    class DataSrc1 implements IDataSource {
      public listeners: DataChangeListener[] = [];
      public data: string[] = [];
    
      public totalCount(): number {
        return this.data.length;
      }
    
      public getData(index: number): string {
        return this.data[index];
      }
    
      // Called by the framework to add a listener to the data source for LazyForEach.
      registerDataChangeListener(listener: DataChangeListener): void {
        if (this.listeners.indexOf(listener) < 0) {
          this.listeners.push(listener);
        }
      }
    
      // Called by the framework to remove the listener from the data source for the corresponding LazyForEach component.
      unregisterDataChangeListener(listener: DataChangeListener): void {
        const pos = this.listeners.indexOf(listener);
        if (pos >= 0) {
          this.listeners.splice(pos, 1);
        }
      }
    
      // Notify LazyForEach that all child components need to be reloaded.
      notifyDataReload(): void {
        this.listeners.forEach(listener => {
          listener.onDataReloaded();
        });
      }
    
      // Notify LazyForEach that a child component needs to be added at the specified index.
      notifyDataAdd(index: number): void {
        this.listeners.forEach(listener => {
          listener.onDataAdd(index);
        });
      }
    
      // Notify LazyForEach that the data item at the specified index has changed and the child component needs to be rebuilt.
      notifyDataChange(index: number): void {
        this.listeners.forEach(listener => {
          listener.onDataChange(index);
        });
      }
    
      // Notify LazyForEach that the child component at the specified index needs to be deleted.
      notifyDataDelete(index: number): void {
        this.listeners.forEach(listener => {
          listener.onDataDelete(index);
        });
      }
    
      // Notify LazyForEach that data needs to be swapped between the from and to positions.
      notifyDataMove(from: number, to: number): void {
        this.listeners.forEach(listener => {
          listener.onDataMove(from, to);
        });
      }
    }
    
    class DataSrc2 implements IDataSource {
      public listeners: DataChangeListener[] = [];
      public data: DataSrc1[] = [];
    
      public totalCount(): number {
        return this.data.length;
      }
    
      public getData(index: number): DataSrc1 {
        return this.data[index];
      }
    
      // Called by the framework to add a listener to the data source for LazyForEach.
      registerDataChangeListener(listener: DataChangeListener): void {
        if (this.listeners.indexOf(listener) < 0) {
          this.listeners.push(listener);
        }
      }
    
      // Called by the framework to remove the listener from the data source for the corresponding LazyForEach component.
      unregisterDataChangeListener(listener: DataChangeListener): void {
        const pos = this.listeners.indexOf(listener);
        if (pos >= 0) {
          this.listeners.splice(pos, 1);
        }
      }
    
      // Notify LazyForEach that all child components need to be reloaded.
      notifyDataReload(): void {
        this.listeners.forEach(listener => {
          listener.onDataReloaded();
        });
      }
    
      // Notify LazyForEach that a child component needs to be added at the specified index.
      notifyDataAdd(index: number): void {
        this.listeners.forEach(listener => {
          listener.onDataAdd(index);
        });
      }
    
      // Notify LazyForEach that the data item at the specified index has changed and the child component needs to be rebuilt.
      notifyDataChange(index: number): void {
        this.listeners.forEach(listener => {
          listener.onDataChange(index);
        });
      }
    
      // Notify LazyForEach that the child component at the specified index needs to be deleted.
      notifyDataDelete(index: number): void {
        this.listeners.forEach(listener => {
          listener.onDataDelete(index);
        });
      }
    
      // Notify LazyForEach that data needs to be swapped between the from and to positions.
      notifyDataMove(from: number, to: number): void {
        this.listeners.forEach(listener => {
          listener.onDataMove(from, to);
        });
      }
    }
    

Scenarios Involving Multiple Item Types

Standard

Reusable components have the same layout. For implementation examples, see the description in the list scrolling sections.

Limited Variation

There are differences between reusable components, but the number of types is limited. For example, reuse can be achieved by explicitly setting two reuseId values or using two custom components.

class LimitedMyDataSource implements IDataSource {
  private dataArray: string[] = [];
  private listener: DataChangeListener | undefined;

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
  }

  public reloadListener(): void {
    this.listener?.onDataReloaded();
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  public unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct LimitedIndex {
  private data: LimitedMyDataSource = new LimitedMyDataSource();

  aboutToAppear() {
    for (let i = 0; i < 1000; i++) { // Loop 1000 times.
      this.data.pushData(i + '');
    }
  }

  build() {
    Column() {
      List({ space: 10 }) {
        LazyForEach(this.data, (item: number) => {
          ListItem() {
            ReusableComponent({ item: item })
            // Set two reuseId values with limited variations.
              .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
          }
          .backgroundColor(Color.Orange)
          .width('100%')
        }, (item: number) => item.toString())
      }
      .cachedCount(2)
    }
  }
}

@Reusable
@Component
struct ReusableComponent {
  @State item: number = 0;

  aboutToReuse(params: ESObject) {
    this.item = params.item;
  }

  build() {
    Column() {
      // Render according to type differences inside the component.
      if (this.item % 2 === 0) {
        Text(`Item ${this.item} ReusableComponentOne`)
          .fontSize(20)
          .margin({ left: 10 })
      } else {
        Text(`Item ${this.item} ReusableComponentTwo`)
          .fontSize(20)
          .margin({ left: 10 })
      }
    }.margin({ left: 10, right: 10 })
  }
}

Composite

There are multiple differences between reusable components, but they usually share common child components. In the example, after three reusable components are converted into Builder functions in a combined manner, the internal shared child components will be uniformly placed under the parent component MyComponent. The reuse cache is shared at the parent component level for child component reuse, reducing resource consumption during component creation.

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
const NUMBER3 = 3;
const NUMBER5 = 5;

class MyDataSource implements IDataSource {
  private dataArray: string[] = [];
  private listener: DataChangeListener | undefined;

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
  }

  public reloadListener(): void {
    this.listener?.onDataReloaded();
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  public unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i < 1000; i++) { // Loop 1000 times.
      this.data.pushData(i.toString());
    }
  }

  // The reusable component implementation of itemBuilderOne is not shown. Below is the Builder version after conversion.
  @Builder
  itemBuilderOne(item: string) {
    Column() {
      ChildComponentA({ item: item });
      ChildComponentB({ item: item });
      ChildComponentC({ item: item });
    }
  }

  // Builder version of itemBuilderTwo after conversion.
  @Builder
  itemBuilderTwo(item: string) {
    Column() {
      ChildComponentA({ item: item });
      ChildComponentC({ item: item });
      ChildComponentD({ item: item });
    }
  }

  // Builder version of itemBuilderThree after conversion.
  @Builder
  itemBuilderThree(item: string) {
    Column() {
      ChildComponentA({ item: item });
      ChildComponentB({ item: item });
      ChildComponentD({ item: item });
    }
  }

  build() {
    List({ space: 40 }) {
      LazyForEach(this.data, (item: string, index: number) => {
        ListItem() {
          if (index % NUMBER3 === 0) {
            this.itemBuilderOne(item);
          } else if (index % NUMBER5 === 0) {
            this.itemBuilderTwo(item);
          } else {
            this.itemBuilderThree(item);
          }
        }
        .backgroundColor('#cccccc')
        .width('100%')
        .onAppear(() => {
          hilog.info(DOMAIN, TAG, BUNDLE + `ListItem ${index} onAppear`);
        })
      }, (item: number) => item.toString())
    }
    .width('100%')
    .height('100%')
    .cachedCount(0)
  }
}

@Reusable
@Component
struct ChildComponentA {
  @State item: string = '';

  aboutToReuse(params: ESObject) {
    hilog.info(DOMAIN, TAG, BUNDLE + `ChildComponentA ${params.item} Reuse ${this.item}`);
    this.item = params.item;
  }

  aboutToRecycle(): void {
    hilog.info(DOMAIN, TAG, BUNDLE + `ChildComponentA ${this.item} Recycle`);
  }

  build() {
    Column() {
      Text(`Item ${this.item} Child Component A`)
        .fontSize(20)
        .margin({ left: 10 })
        .fontColor(Color.Blue)
      Grid() {
        ForEach((new Array(20)).fill(''), (item: string, index: number) => {
          GridItem() {
            // Ensure that the app.media.startIcon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error.
            Image($r('app.media.startIcon'))
              .height(20)
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr 1fr')
      .columnsGap(10)
      .width('90%')
      .height(160)
    }
    .margin({ left: 10, right: 10 })
    .backgroundColor(0xFAEEE0)
  }
}

@Reusable
@Component
struct ChildComponentB {
  @State item: string = '';

  aboutToReuse(params: ESObject) {
    this.item = params.item;
  }

  build() {
    Row() {
      Text(`Item ${this.item} Child Component B`)
        .fontSize(20)
        .margin({ left: 10 })
        .fontColor(Color.Red)
    }.margin({ left: 10, right: 10 })
  }
}

@Reusable
@Component
struct ChildComponentC {
  @State item: string = '';

  aboutToReuse(params: ESObject) {
    this.item = params.item;
  }

  build() {
    Row() {
      Text(`Item ${this.item} Child Component C`)
        .fontSize(20)
        .margin({ left: 10 })
        .fontColor(Color.Green)
    }.margin({ left: 10, right: 10 })
  }
}

@Reusable
@Component
struct ChildComponentD {
  @State item: string = '';

  aboutToReuse(params: ESObject) {
    this.item = params.item;
  }

  build() {
    Row() {
      Text(`Item ${this.item} Child Component D`)
        .fontSize(20)
        .margin({ left: 10 })
        .fontColor(Color.Orange)
    }.margin({ left: 10, right: 10 })
  }
}