Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

HarmonyOS ArkTS State Management: A Comprehensive Guide to Decorators

Tech 2

Overview

ArkUI provides a variety of decorators for state management, primarily divided into three categories: managing component-owned state, managing application-owned state, and other state management features. The following diagram illustrates the main decorators:

Decorator Overview

Managing Component-Owned State

Decorator Description Use Case
@State Manages state within a component. Serves as a data source for one-way and two-way synchronization with child components, triggering re-rendering. Basic component state.
@Prop Establishes one-way synchronization from parent to child. Changes in the child do not propagate back to the parent. Passing initial values from parent to child.
@Link Establishes two-way synchronization between parent and child. Changes in either component are reflected in the other. Shared state that needs to be kept in sync.
@Provide/@Consume Synchronizes state across component hierarchies without explicit parameter passing. Cross-level state sharing.
@Observed Decorates classes to enable deep observation of nested objects. Must be used with @ObjectLink or @Prop. Observing changes in nested class properties.
@ObjectLink Receives instances of @Observed classes for two-way synchronization with parent data sources. Deep synchronization of nested objects.

@State: Component Internal State

Characteristics:

  • Private: @State variables are only accessible within the component.
  • Initialization: Must be initialized locally with a type and value. Can be overridden by parent component via named parameters.
  • Data Synchronization: Can establish one-way or two-way synchronization with @Prop, @Link, or @ObjectLink in child components.
  • Lifecycle: Same as the lifecycle of the owning custom component.

Supported types: Object, class, string, number, boolean, enum, and arrays of these. Complex types like Date are not supported.

Parent-Child Initialization and Data Flow: @State Data Flow

Change Detection Rules
  1. Simple types (boolean, string, number):
@State count: number = 0;
// Direct assignment is observed
this.count = 1;
  1. Class or Object types:
class ClassA {
  public value: string;
  constructor(value: string) {
    this.value = value;
  }
}

class Model {
  public value: string;
  public name: ClassA;
  constructor(value: string, a: ClassA) {
    this.value = value;
    this.name = a;
  }
}

@State title: Model = new Model('Hello', new ClassA('World'));

// Assigning a new object is observed
this.title = new Model('Hi', new ClassA('ArkUI'));

// Assigning a property is observed
this.title.value = 'Hi';

// Nested property assignment is NOT observed unless @Observed is used
this.title.name.value = 'ArkUI';
  1. Array types:
class Model {
  public value: number;
  constructor(value: number) {
    this.value = value;
  }
}

@State title: Model[] = [new Model(11), new Model(1)];

// Array replacement
this.title = [new Model(2)];

// Index assignment
this.title[0] = new Model(2);

// Array mutation methods
this.title.pop();
this.title.push(new Model(12));
Usage Scenarios
  1. Simple type:
@Entry
@Component
struct Counter {
  @State count: number = 0;

  build() {
    Button(`Clicked ${this.count} times`)
      .onClick(() => {
        this.count += 1;
      });
  }
}
  1. Other types:
class Model {
  public value: string;
  constructor(value: string) {
    this.value = value;
  }
}

@Entry
@Component
struct EntryComponent {
  build() {
    Column() {
      MyComponent({ count: 1, increaseBy: 2 })
      MyComponent({ title: new Model('Hello, World 2'), count: 7 })
    }
  }
}

@Component
struct MyComponent {
  @State title: Model = new Model('Hello World');
  @State count: number = 0;
  private increaseBy: number = 1;

  build() {
    Column() {
      Text(`${this.title.value}`)
      Button('Change Title').onClick(() => {
        this.title.value = (this.title.value === 'Hello ArkUI') ? 'Hello World' : 'Hello ArkUI';
      })
      Button(`Count=${this.count}`).onClick(() => {
        this.count += this.increaseBy;
      })
    }
  }
}

@Prop: One-Way Synchronization from Parent to Child

Characteristics:

  • One-way sync: Parent changes are reflected in child; child changes do not affect parent.
  • Local modification: @Prop variables can be modified locally, but changes are not sent back.
  • Automatic update: When parent data source changes, @Prop variables are automatically updated.
  • Overwrite: If child has already modified the @Prop variable, a subsequent parent update will overwrite the child's local change.
  • Limitations: @Prop performs a deep copy for complex types, losing type information except for basic types, Map, Set, Date, and Array. @Prop cannot be used in an @Entry component.

Supported types: string, number, boolean, enum, and arrays of these. Complex types like any are not supported.

Data Flow Diagram: @Prop Data Flow

Change Detection Rules
@Prop count: number;
this.count = 1; // Change is observed

For synchronization between @State and @Prop:

  • Parent's @State initializes child's @Prop. When @State changes, @Prop is updated.
  • Modifications to @Prop do not affect the parent's @State.
  • Data source can also be @Link or @Prop, with the same synchronization mechanism.
  • Data source and @Prop must have the same type.
Usage Scenarios
  1. Simple type synchronization from parent @State to child @Prop:
@Component
struct CountDownComponent {
  @Prop count: number;
  costOfOneAttempt: number = 1;

  build() {
    Column() {
      if (this.count > 0) {
        Text(`You have ${this.count} Nuggets left`)
      } else {
        Text('Game over!')
      }
      Button('Try again').onClick(() => {
        this.count -= this.costOfOneAttempt;
      })
    }
  }
}

@Entry
@Component
struct ParentComponent {
  @State countDownStartValue: number = 10;

  build() {
    Column() {
      Text(`Grant ${this.countDownStartValue} nuggets to play.`)
      Button('+1 - Nuggets in New Game').onClick(() => {
        this.countDownStartValue += 1;
      })
      Button('-1 - Nuggets in New Game').onClick(() => {
        this.countDownStartValue -= 1;
      })
      CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
    }
  }
}
  1. Array item synchronization from parent @State to child @Prop:
@Component
struct NumberDisplay {
  @Prop value: number;

  build() {
    Text(`${this.value}`).fontSize(50).onClick(() => { this.value++; })
  }
}

@Entry
@Component
struct Index {
  @State arr: number[] = [1, 2, 3];

  build() {
    Row() {
      Column() {
        NumberDisplay({ value: this.arr[0] })
        NumberDisplay({ value: this.arr[1] })
        NumberDisplay({ value: this.arr[2] })
        Divider().height(5)
        ForEach(this.arr, item => NumberDisplay({ value: item }), item => item.toString())
        Text('Replace entire arr').fontSize(50).onClick(() => {
          this.arr = (this.arr[0] === 1) ? [3, 4, 5] : [1, 2, 3];
        })
      }
    }
  }
}
  1. Class property synchronization from parent @State to child @Prop:
class Book {
  public title: string;
  public pages: number;
  public readIt: boolean = false;

  constructor(title: string, pages: number) {
    this.title = title;
    this.pages = pages;
  }
}

@Component
struct ReaderComponent {
  @Prop title: string;
  @Prop readIt: boolean;

  build() {
    Row() {
      Text(this.title)
      Text(`... ${this.readIt ? 'I have read' : 'I have not read it'}`)
        .onClick(() => this.readIt = true)
    }
  }
}

@Entry
@Component
struct Library {
  @State book: Book = new Book('100 secrets of C++', 765);

  build() {
    Column() {
      ReaderComponent({ title: this.book.title, readIt: this.book.readIt })
      ReaderComponent({ title: this.book.title, readIt: this.book.readIt })
    }
  }
}
  1. @Prop with local initialization (not synced from parent):
@Component
struct MyComponent {
  @Prop customCounter: number;
  @Prop customCounter2: number = 5; // local default

  build() {
    Column() {
      Row() {
        Text(`From Main: ${this.customCounter}`).width(90).height(40).fontColor('#FF0010')
      }
      Row() {
        Button('Click to change locally!').width(180).height(60).margin({ top: 10 })
          .onClick(() => {
            this.customCounter2++
          })
      }.height(100).width(180)
      Row() {
        Text(`Custom Local: ${this.customCounter2}`).width(90).height(40).fontColor('#FF0010')
      }
    }
  }
}

@Entry
@Component
struct MainProgram {
  @State mainCounter: number = 10;

  build() {
    Column() {
      Button('Click to change number').width(480).height(60).margin({ top: 10, bottom: 10 })
        .onClick(() => {
          this.mainCounter++
        })
      MyComponent({ customCounter: this.mainCounter })
      MyComponent({ customCounter: this.mainCounter, customCounter2: this.mainCounter })
    }
  }
}

@Link: Two-Way Synchronization between Parent and Child

@Link can establish two-way data synchronization with parent's @State, @StorageLink, or @Link.

Supported types: string, number, boolean, enum, and arrays of these. Complex types like any are not supported.

Data Flow Diagram: @Link Data Flow

Change Detection Rules
  • For boolean, string, number: value changes are observed.
  • For class or Object: assignment and property assignment are observed (all properties returned by Object.keys).
  • For array: additions, deletions, and element updates are observed.
Usage Scenarios
  1. Simple type and class object @Link:
class GreenButtonState {
  width: number = 0;
  constructor(width: number) {
    this.width = width;
  }
}

@Component
struct GreenButton {
  @Link greenButtonState: GreenButtonState;

  build() {
    Button('Green Button')
      .width(this.greenButtonState.width)
      .height(150.0)
      .backgroundColor('#00ff00')
      .onClick(() => {
        if (this.greenButtonState.width < 700) {
          this.greenButtonState.width += 125;
        } else {
          this.greenButtonState = new GreenButtonState(100);
        }
      })
  }
}

@Component
struct YellowButton {
  @Link yellowButtonState: number;

  build() {
    Button('Yellow Button')
      .width(this.yellowButtonState)
      .height(150.0)
      .backgroundColor('#ffff00')
      .onClick(() => {
        this.yellowButtonState += 50.0;
      })
  }
}

@Entry
@Component
struct ShufflingContainer {
  @State greenButtonState: GreenButtonState = new GreenButtonState(300);
  @State yellowButtonProp: number = 100;

  build() {
    Column() {
      Button('Parent: Set yellowButton').onClick(() => {
        this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 100 : 100;
      })
      Button('Parent: Set GreenButton').onClick(() => {
        this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100;
      })
      GreenButton({ greenButtonState: $greenButtonState })
      YellowButton({ yellowButtonState: $yellowButtonProp })
    }
  }
}
  1. Array @Link:
@Component
struct Child {
  @Link items: number[];

  build() {
    Column() {
      Button('Push').onClick(() => {
        this.items.push(this.items.length + 1);
      })
      Button('Replace whole array').onClick(() => {
        this.items = [100, 200, 300];
      })
    }
  }
}

@Entry
@Component
struct Parent {
  @State arr: number[] = [1, 2, 3];

  build() {
    Column() {
      Child({ items: $arr })
      ForEach(this.arr, item => Text(`${item}`), item => item.toString())
    }
  }
}

@Provide/@Consume: Two-Way Synchronization with Descendant Components

Characteristics:

  • Two-way data sync: @Provide and @Consume enable bidirectional data synchronization between ancestor and descendant components.
  • Cross-level: They allow state sharing across multiple levels without explicit parameter passing.
  • Binding: Multiple @Consume can bind to the same @Provide by matching variable name or alias.
  • Limitation: Cannot have multiple @Provide with the same name or alias within the same component tree.

Supported types: string, number, boolean, enum, and arrays of these. Complex types like any are not supported.

Data Flow Diagram: @Provide/@Consume Data Flow

Change Detection Rules
  • For boolean, string, number: value changes are observed.
  • For class or Object: assignment and property assignment are observed (all properties returned by Object.keys).
  • For array: additions, deletions, and element updates are observed.
Usage Scenario
@Component
struct DeepChild {
  @Consume reviewVotes: number;

  build() {
    Column() {
      Text(`reviewVotes(${this.reviewVotes})`)
      Button(`Give +1`).onClick(() => this.reviewVotes += 1)
    }.width('50%')
  }
}

@Component
struct MiddleChild {
  build() {
    Row({ space: 5 }) {
      DeepChild()
      DeepChild()
    }
  }
}

@Component
struct OuterChild {
  build() {
    MiddleChild()
  }
}

@Entry
@Component
struct Root {
  @Provide reviewVotes: number = 0;

  build() {
    Column() {
      Button(`reviewVotes(${this.reviewVotes}), +1`).onClick(() => this.reviewVotes += 1)
      OuterChild()
    }
  }
}

@Observed/@ObjectLink: Observing Changes in Nested Class Properties

Characteristics:

  • Two-way data sync: @ObjectLink and @Observed work together for bidirectional synchronization in nested object or array scenarios.
  • Observing property changes: @Observed decorated classes allow observation of property changes.
  • Instance binding: @ObjectLink decorated variables in child components receive instances of @Observed classes, establishing two-way binding with the parent's corresponding state.
  • Usage: @Observed alone has no effect; it must be used with @ObjectLink or @Prop.
  • Limitation: @Observed changes the prototype chain of the class. @ObjectLink cannot be used in an @Entry component.

Type must be an @Observed decorated class. Can be used to initialize regular variables, @State, @Link, @Prop, @Provide.

Nested Class Data Flow Diagram: @Observed/@ObjectLink Data Flow

Change Detection Rules
class ClassA {
  public c: number;
  constructor(c: number) {
    this.c = c;
  }
}

@Observed
class ClassB {
  public a: ClassA;
  public b: number;
  constructor(a: ClassA, b: number) {
    this.a = a;
    this.b = b;
  }
}

// @ObjectLink b: ClassB
// Assignment to property is observed
this.b.a = new ClassA(5);
this.b.b = 5;

// Change in non-@Observed nested class property is NOT observed
this.b.a.c = 5;
Usage Scenarios
  1. Nested Object:
let NextID: number = 1;

@Observed
class ClassA {
  public id: number;
  public c: number;
  constructor(c: number) {
    this.id = NextID++;
    this.c = c;
  }
}

@Observed
class ClassB {
  public a: ClassA;
  constructor(a: ClassA) {
    this.a = a;
  }
}

@Component
struct ViewA {
  label: string = 'ViewA1';
  @ObjectLink a: ClassA;

  build() {
    Row() {
      Button(`ViewA [${this.label}] this.a.c=${this.a.c} +1`)
        .onClick(() => {
          this.a.c += 1;
        })
    }
  }
}

@Entry
@Component
struct ViewB {
  @State b: ClassB = new ClassB(new ClassA(0));

  build() {
    Column() {
      ViewA({ label: 'ViewA #1', a: this.b.a })
      ViewA({ label: 'ViewA #2', a: this.b.a })
      Button('ViewB: this.b.a.c += 1').onClick(() => {
        this.b.a.c += 1;
      })
      Button('ViewB: this.b.a = new ClassA(0)').onClick(() => {
        this.b.a = new ClassA(0);
      })
      Button('ViewB: this.b = new ClassB(ClassA(0))').onClick(() => {
        this.b = new ClassB(new ClassA(0));
      })
    }
  }
}
  1. Array of Objects:
@Component
struct ViewA {
  @ObjectLink a: ClassA;
  label: string = 'ViewA1';

  build() {
    Row() {
      Button(`ViewA [${this.label}] this.a.c = ${this.a.c} +1`)
        .onClick(() => {
          this.a.c += 1;
        })
    }
  }
}

@Entry
@Component
struct ViewB {
  @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)];

  build() {
    Column() {
      ForEach(this.arrA, item => ViewA({ label: `#${item.id}`, a: item }), item => item.id.toString())
      ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })
      ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length - 1] })
      Button('ViewB: reset array').onClick(() => {
        this.arrA = [new ClassA(0), new ClassA(0)];
      })
      Button('ViewB: push').onClick(() => {
        this.arrA.push(new ClassA(0));
      })
      Button('ViewB: shift').onClick(() => {
        this.arrA.shift();
      })
      Button('ViewB: change item property in middle').onClick(() => {
        this.arrA[Math.floor(this.arrA.length / 2)].c = 10;
      })
      Button('ViewB: replace item in middle').onClick(() => {
        this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);
      })
    }
  }
}
  1. 2D Array:
@Observed
class StringArray extends Array<string> {}

@Component
struct ItemPage {
  @ObjectLink itemArr: StringArray;

  build() {
    Row() {
      Text('ItemPage').width(100).height(100)
      ForEach(this.itemArr, item => Text(item).width(100).height(100), item => item)
    }
  }
}

@Entry
@Component
struct IndexPage {
  @State arr: StringArray[] = [new StringArray(), new StringArray(), new StringArray()];

  build() {
    Column() {
      ItemPage({ itemArr: this.arr[0] })
      ItemPage({ itemArr: this.arr[1] })
      ItemPage({ itemArr: this.arr[2] })
      Divider()
      ForEach(this.arr, itemArr => ItemPage({ itemArr: itemArr }), itemArr => itemArr[0])
      Divider()
      Button('Update').onClick(() => {
        console.error('Update all items in arr');
        if (this.arr[0][0] !== undefined) {
          this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`);
          this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`);
          this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`);
        } else {
          this.arr[0].push('Hello');
          this.arr[1].push('World');
          this.arr[2].push('!');
        }
      })
    }
  }
}

Managing Application-Owned State

Storage Type Description
LocalStorage Page-level UI state storage for sharing state within a UIAbility and across pages.
AppStorage Singleton LocalStorage created by the UI framework at app startup for centralized storage.
PersistentStorage Persists UI state to disk, usually used with AppStorage to ensure data survives app restarts.
Environment Device environment parameters that sync to AppStorage.

LocalStorage: Page-Level UI State Storage

Change Detection Rules
Decorator Description
@LocalStorageProp One-way synchronization from LocalStorage to the component.
@LocalStorageLink Two-way synchronization between LocalStorage and the component.
Limitation The type of a named property cannot be changed after LocalStorage creation. Set must use the same type. LocalStorage is page-level; GetShared only works if windowStage.loadContent provided the instance.
  • Changes to @LocalStorageLink are synced back to LocalStorage.
  • Changes to LocalStorage property are synced to all bound @LocalStorageLink and @LocalStorageProp variables.
  • @LocalStorageLink changes trigger re-rendering of the component.
Usage Scenarios
  1. Using LocalStorage in application logic:
let storage = new LocalStorage({ 'PropA': 47 });
let propA = storage.get('PropA'); // propA == 47
let link1 = storage.link('PropA'); // link1.get() == 47
let link2 = storage.link('PropA'); // link2.get() == 47
let prop = storage.prop('PropA'); // prop.get() = 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get()=1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
  1. Using LocalStorage from within UI:
let storage = new LocalStorage({ 'PropA': 47 });

@Component
struct Child {
  @LocalStorageLink('PropA') storLink2: number = 1;

  build() {
    Button(`Child from LocalStorage ${this.storLink2}`)
      .onClick(() => this.storLink2 += 1)
  }
}

@Entry(storage)
@Component
struct CompA {
  @LocalStorageLink('PropA') storLink1: number = 1;

  build() {
    Column({ space: 15 }) {
      Button(`Parent from LocalStorage ${this.storLink1}`)
        .onClick(() => this.storLink1 += 1)
      Child()
    }
  }
}
  1. One-way synchronization with @LocalStorageProp:
let storage = new LocalStorage({ 'PropA': 47 });

@Entry(storage)
@Component
struct CompA {
  @LocalStorageProp('PropA') storProp1: number = 1;

  build() {
    Column({ space: 15 }) {
      Button(`Parent from LocalStorage ${this.storProp1}`)
        .onClick(() => this.storProp1 += 1)
      Child()
    }
  }
}

@Component
struct Child {
  @LocalStorageProp('PropA') storProp2: number = 2;

  build() {
    Column({ space: 15 }) {
      Text(`Parent from LocalStorage ${this.storProp2}`)
    }
  }
}
  1. Two-way synchronization with @LocalStorageLink:
let storage = new LocalStorage({ 'PropA': 47 });
let linkToPropA = storage.link('PropA');

@Entry(storage)
@Component
struct CompA {
  @LocalStorageLink('PropA') storLink: number = 1;

  build() {
    Column() {
      Text('incr @LocalStorageLink variable')
        .onClick(() => this.storLink += 1)
      Text(`@LocalStorageLink: ${this.storLink} - linkToPropA: ${linkToPropA.get()}`)
    }
  }
}
  1. Synchronizing state between sibling components:
let storage = new LocalStorage({ countStorage: 1 });

@Component
struct Child {
  label: string = 'no name';
  @LocalStorageLink('countStorage') playCountLink: number = 0;

  build() {
    Row() {
      Text(this.label).width(50).height(60).fontSize(12)
      Text(`playCountLink ${this.playCountLink}: inc by 1`)
        .onClick(() => { this.playCountLink += 1 })
        .width(200).height(60).fontSize(12)
    }.width(300).height(60)
  }
}

@Entry(storage)
@Component
struct Parent {
  @LocalStorageLink('countStorage') playCount: number = 0;

  build() {
    Column() {
      Row() {
        Text('Parent').width(50).height(60).fontSize(12)
        Text(`playCount ${this.playCount} dec by 1`)
          .onClick(() => { this.playCount -= 1 })
          .width(250).height(60).fontSize(12)
      }.width(300).height(60)
      Row() {
        Text('LocalStorage').width(50).height(60).fontSize(12)
        Text(`countStorage ${this.playCount} incr by 1`)
          .onClick(() => { storage.set('countStorage', 1 + storage.get('countStorage')); })
          .width(250).height(60).fontSize(12)
      }.width(300).height(60)
      Child({ label: 'ChildA' })
      Child({ label: 'ChildB' })
      Text(`playCount in LocalStorage for debug ${storage.get<number>('countStorage')}`)
        .width(300).height(60).fontSize(12)
    }
  }
}
  1. Sharing LocalStorage from UIAbility to multiple views:
// EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

let para: Record<string, number> = { 'PropA': 47 };
let localStorage: LocalStorage = new LocalStorage(para);

export default class EntryAbility extends UIAbility {
  storage: LocalStorage = localStorage;

  onWindowStageCreate(windowStage: window.WindowStage) {
    windowStage.loadContent('pages/Index', this.storage);
  }
}
// Index.ets
let storage = LocalStorage.GetShared();

@Entry(storage)
@Component
struct CompA {
  @LocalStorageLink('PropA') varA: number = 1;

  build() {
    Column() {
      Text(`${this.varA}`).fontSize(50)
    }
  }
}

AppStorage: Application-Level Storage

(Detailed content continues...)


Note: This guide covers the core concepts of state management in ArkTS for HarmonyOS. For more advanced topics, refer to the official documentation.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.