ArkTS Component Development Basics: Project Creation, Decorators, Styles, Events, State Management, and Slots
This tutorial walks through the fundamentals of building ArkTS components for HarmonyOS, covering project setup, key decorators like @Entry and @Component, styling with @Styles and @Extend, handling events, managing properties and state, conditional rendering, functional components, and slot implementation.
Introduction
I had the opportunity to participate in the HarmonyOS conversion of our factory app, learning ArkTS and the related IDE, and exchanging ideas with HarmonyOS experts on the issue tracker.
This article, from a web‑frontend perspective, briefly discusses basic ArkTS component development topics such as property passing, slots, and conditional rendering.
Creating a Project
The project creation process is straightforward and therefore omitted.
Component and Page
After creating the project, the IDE automatically opens the initial page. The generated code looks like this:
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
.height('100%')
.width('100%')
}
}The @Entry decorator marks the page as an independent Page that can be navigated to via the router. @Component wraps the object for rendering and creates a MVVM‑like data‑to‑view update flow, similar to React.Component or Vue.defineComponent . The build method corresponds to render() in React or setup() in Vue; it must be declared when @Component is used.
@Entry indicates a standalone page.
@Component enables data‑to‑view binding.
build performs the rendering logic.
Inside build() only declarative expressions are allowed, effectively a JSX‑style variant.
// Example of a simple return expression
export default () => (
Hello World
) @Component
export default struct SomeComponent {
build() {
// console.log(123) // not allowed
Text('Hello World')
}
}Independent Component
The previous example does not use @Entry ; it is a complete component declaration on its own.
Extracting the component into its own file:
@Component
export struct CustomButton {
build() {
Button('My Button')
}
}Using the component with a flex layout:
import { CustomButton } from './CustomButton'
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Text(this.message)
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
CustomButton()
}
.height('100%')
.width('100%')
}
}Style Clusters
Unlike web front‑end CSS, ArkTS does not have a separate CSS file; instead, styles are expressed via chained method calls. The @Styles decorator defines reusable style clusters, while @Extend can extend existing components.
@Styles Decorator
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
// Declare a style cluster
@Styles
HelloWorldStyle() {
.backgroundColor(Color.Yellow)
.border({ width: { bottom: 5 }, color: '#ccc' })
.margin({ bottom: 10 })
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Text(this.message)
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.HelloWorldStyle()
CustomButton()
}
.height('100%')
.width('100%')
}
}@Styles can also decorate a plain function to create a style helper.
@Styles
function HelloWorldStyle2() {
.backgroundColor(Color.Yellow)
.border({ width: { bottom: 5 }, color: '#000' })
.margin({ bottom: 10 })
}For properties that are only available on certain components (e.g., fontSize , fontColor ), @Extend is required.
@Extend Decorator
@Extend(Text)
function TextStyle() {
.fontSize(50)
.fontWeight(FontWeight.Bold)
.id('HelloWorld')
}@Extend can also accept parameters:
@Extend(Text)
function TextStyle(fontSize: number = 50, fontColor: ResourceStr | Color = '#f00') {
.fontSize(fontSize)
.fontColor(fontColor)
.fontWeight(FontWeight.Bold)
.id('HelloWorld')
}Event Callbacks
Events can be attached using the .onClick method. Example using promptAction.showToast to display a toast:
import { promptAction } from '@kit.ArkUI'
@Component
export struct CustomButton {
build() {
Column() {
Button('My Button')
.onClick(() => {
promptAction.showToast({ message: 'You clicked me!' })
})
}
}
}Note that ArkTS does not support event bubbling or capture; events are handled directly on the target component.
Properties and State
In the MV architecture, data models are divided into property (read‑only values passed from parent) and state (private mutable values). ArkTS provides @Prop for properties and @State for internal state, similar to React's props and useState .
@State Decorator
The message field in earlier examples is declared with @State , making it reactive.
@Prop Decorator
Parent components can pass values to child components via @Prop . Example adding a text property to CustomButton :
@Component
export struct CustomButton {
@Prop text: string = 'My Button'
build() {
Column() {
Button(this.text)
.onClick(() => { /* ... */ })
}
}
}Usage from the parent:
CustomButton({ text: 'My Button' })Changing Properties and State
By exposing a count property and a custom onClickMyButton event, the parent can synchronize a click counter with the child component.
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
@State clickCount: number = 0;
@Styles
HelloWorldStyle() { /* ... */ }
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Text(this.message)
.TextStyle(36, '#06c')
.HelloWorldStyle2()
CustomButton({
text: 'Click Count',
count: this.clickCount,
onClickMyButton: () => { this.clickCount += 1 }
})
}
.height('100%')
.width('100%')
}
}@Watch Decorator
@Watch can monitor changes to a @Prop and invoke a handler:
@Prop @Watch('onChange') count: number = 0
private onChange(propName: string) {
console.log('>>>', propName)
}When the property changes, onChange receives the property name, allowing a single method to handle multiple watched properties.
Conditional Rendering
ArkTS supports classic if…else statements inside build() , avoiding the limitations of JSX ternary expressions.
build() {
Column() {
Button(`${this.text}(${this.count} x 2 = ${this.double})`).onClick(() => { /* ... */ })
if (this.count % 2 === 0) {
Text('Even').fontColor(Color.Red).margin({ top: 10 })
} else {
Text('Odd').fontColor(Color.Blue).margin({ top: 10 })
}
}
.onAttach(() => { this.onChange() })
}Functional Components
Functions annotated with @Builder become reusable UI fragments. Arrow functions are **not** supported; only regular function declarations can be used.
@Builder
function ItalicText(content: string) {
Text(content).fontSize(14).fontStyle(FontStyle.Italic).margin({ bottom: 10 })
}The builder can be invoked inside build() just like a component.
Implementing Slots
ArkTS provides the @BuilderParam decorator to allow a component to accept a slot function. The slot is invoked inside the component’s layout.
@Component
export struct CustomButton {
@Prop text: string = 'My Button'
@Prop @Watch('onChange') count: number = 0
@State private double: number = 0
@BuilderParam slot: () => void
private onChange() { this.double = this.count * 2 }
build() {
Column() {
Button(`${this.text}(${this.count} x 2 = ${this.double})`).onClick(() => { if (typeof this.onClickMyButton === 'function') this.onClickMyButton() })
if (typeof this.slot === 'function') this.slot()
}
.onAttach(() => { this.onChange() })
}
}Parent usage with a slot:
CustomButton({
text: 'Click Count',
count: this.clickCount,
onClickMyButton: () => { this.clickCount += 1 },
slot: () => { this.SubTitle() }
})Only one parameter‑less @BuilderParam is allowed per component; multiple slots require distinct names and must be invoked separately.
Conclusion
For developers already familiar with TypeScript, ArkTS feels approachable after a brief overview of its syntax, decorators, and standard library. The main pain points are limited type inference, incomplete documentation, and the lack of hot‑reload in the simulator.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.