HarmonyOS开发:状态管理迁移——Redux/MobX到ArkTS状态
HarmonyOS开发:状态管理迁移——Redux/MobX到ArkTS状态
📌 核心要点:ArkTS内置的状态管理系统比Redux/MobX更轻量,@State/@Link/@Trace替代了Store/Reducer/Action的样板代码,但你需要重新理解"谁拥有状态"这个核心问题。
背景与动机
你写前端或者React Native,Redux用了三年,Action/Reducer/Middleware那套流程刻在DNA里了。或者你用MobX,@observable/@computed/@action写得飞起。
现在要迁移鸿蒙,你第一反应可能是:“鸿蒙有没有Redux?能不能装个MobX?”
别找了。鸿蒙有自己的状态管理系统,而且不需要第三方库。
ArkTS的状态管理是语言级内置的——@State、@Link、@Prop、@Trace、AppStorage,这些不是库,是框架的一部分。你不需要写Action/Reducer,不需要配置Store,状态变化自动触发UI更新。
但"内置"不等于"简单"。ArkTS的状态管理有自己的规则,特别是状态所有权和传递方向,和Redux/MobX的设计哲学差异很大。不理解这些规则,你的代码会到处报错,而且错误信息还不好懂。
这篇文章,帮你把Redux/MobX的思维模式翻译成ArkTS的。
核心原理
状态管理哲学对比
graph TB
subgraph Redux["Redux 模式"]
R1[Component dispatch Action] --> R2[Reducer 纯函数]
R2 --> R3[Store 单一状态树]
R3 --> R4[Component 订阅更新]
end
subgraph MobX["MobX 模式"]
M1[Component 调用 action] --> M2[Observable 可观察对象]
M2 --> M3[Computed 计算属性]
M3 --> M4[Observer 自动追踪]
end
subgraph ArkTS["ArkTS 状态管理"]
A1["@State/@Trace 状态定义"] --> A2[框架自动追踪依赖]
A2 --> A3[状态变化触发更新]
A3 --> A4["@Link/@Prop 子组件同步"]
end
R3 -.->|单一状态树| A1
M2 -.->|可观察对象| A1
classDef reduxStyle fill:#764ABC,stroke:#5A32A3,color:#fff,font-weight:bold
classDef mobxStyle fill:#FF9950,stroke:#CC7A3D,color:#000,font-weight:bold
classDef arkStyle fill:#0A59F7,stroke:#0A3CC7,color:#fff,font-weight:bold
class R1,R2,R3,R4 reduxStyle
class M1,M2,M3,M4 mobxStyle
class A1,A2,A3,A4 arkStyle
核心概念映射
| Redux/MobX概念 | ArkTS对应 | 说明 |
|---|---|---|
| Store(单一状态树) | AppStorage | 全局状态容器 |
| State(状态切片) | @State/@Trace | 组件级状态 |
| Action(状态变更描述) | 直接修改属性 | 不需要Action描述 |
| Reducer(纯函数) | 属性赋值 | 框架自动处理 |
| Middleware(中间件) | 无直接对应 | 用拦截器或装饰器实现 |
| @observable | @Trace | 可观察属性 |
| @computed | getter方法 | 计算属性 |
| @action | 普通方法 | 不需要装饰器 |
| useSelector | @StorageLink/@StorageProp | 全局状态订阅 |
| connect | @Link/@Prop | 父子组件状态传递 |
ArkTS状态装饰器全景
| 装饰器 | 作用域 | 数据流向 | 允许修改 |
|---|---|---|---|
| @State | 组件内部 | 单向(自身→UI) | 是 |
| @Prop | 父→子 | 单向(父→子) | 否(本地修改不回传) |
| @Link | 父↔子 | 双向(父↔子) | 是 |
| @Provide | 祖先→后代 | 跨层级传递 | 是 |
| @Consume | 后代←祖先 | 跨层级接收 | 是 |
| @Watch | 监听变化 | 触发回调 | — |
| @Trace | @ObservedV2类 | 深度观测 | 是 |
| @StorageLink | AppStorage | 双向绑定 | 是 |
| @StorageProp | AppStorage | 单向绑定 | 否 |
代码实战
基础用法:Redux迁移到@State
先看一个典型的Redux实现:
// Redux: 用户模块
// Action Types
const SET_USER = 'SET_USER'
const UPDATE_NAME = 'UPDATE_NAME'
const LOGOUT = 'LOGOUT'
// Action Creators
const setUser = (user) => ({ type: SET_USER, payload: user })
const updateName = (name) => ({ type: UPDATE_NAME, payload: name })
const logout = () => ({ type: LOGOUT })
// Reducer
const initialState = { user: null, loading: false, error: null }
function userReducer(state = initialState, action) {
switch (action.type) {
case SET_USER:
return { ...state, user: action.payload, loading: false }
case UPDATE_NAME:
return { ...state, user: { ...state.user, name: action.payload } }
case LOGOUT:
return { ...state, user: null }
default:
return state
}
}
// 组件中使用
function UserPage() {
const dispatch = useDispatch()
const user = useSelector(state => state.user.user)
const loading = useSelector(state => state.user.loading)
useEffect(() => {
dispatch({ type: 'SET_LOADING' })
fetchUser().then(data => dispatch(setUser(data)))
}, [])
return (
<View>
{user && <Text>{user.name}</Text>}
<Button onClick={() => dispatch(logout())}>退出</Button>
</View>
)
}
迁移到ArkTS:
// 鸿蒙: 用@ObservedV2替代Redux
@ObservedV2
export class UserStore {
@Trace user: UserInfo | null = null
@Trace loading: boolean = false
@Trace error: string = ''
// 对应Redux的Action + Reducer
async loadUser() {
this.loading = true
this.error = ''
try {
let response = await http.createHttp().request('https://api.example.com/user', {
method: http.RequestMethod.GET
})
if (response.responseCode === 200) {
this.user = JSON.parse(response.result as string) as UserInfo
}
} catch (e) {
this.error = (e as Error).message
} finally {
this.loading = false
}
}
// 对应Redux的updateName Action
updateName(name: string) {
if (this.user) {
this.user = { ...this.user, name: name }
}
}
// 对应Redux的logout Action
logout() {
this.user = null
}
}
// 页面使用
@Entry
@Component
struct UserPage {
@Local userStore: UserStore = new UserStore()
aboutToAppear() {
this.userStore.loadUser()
}
build() {
Column() {
if (this.userStore.loading) {
LoadingProgress()
.width(48)
.height(48)
} else if (this.userStore.user) {
Text(this.userStore.user.name)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Button('退出登录')
.margin({ top: 20 })
.onClick(() => this.userStore.logout())
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
看到了吗?Redux的Action+Reducer+Selector三件套,在ArkTS里变成了方法+属性+@Trace。代码量直接砍半。
进阶用法:MobX迁移到@ObservedV2
MobX和ArkTS的理念更接近——都是"可观察对象+自动追踪"。迁移起来也更自然。
// MobX: 可观察的Store
import { makeAutoObservable } from 'mobx'
class TodoStore {
todos = []
filter = 'all'
constructor() {
makeAutoObservable(this)
}
// @computed
get filteredTodos() {
switch (this.filter) {
case 'done': return this.todos.filter(t => t.done)
case 'pending': return this.todos.filter(t => !t.done)
default: return this.todos
}
}
get pendingCount() {
return this.todos.filter(t => !t.done).length
}
// @action
addTodo(title) {
this.todos.push({ id: Date.now(), title, done: false })
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
setFilter(filter) {
this.filter = filter
}
}
// 组件中使用(React + MobX)
const TodoApp = observer(() => {
const store = useTodoStore()
return (
<View>
<Text>待完成: {store.pendingCount}</Text>
{store.filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={() => store.toggleTodo(todo.id)} />
))}
</View>
)
})
迁移到ArkTS:
// 鸿蒙: @ObservedV2替代MobX
interface TodoItem {
id: number
title: string
done: boolean
}
@ObservedV2
export class TodoStore {
@Trace todos: TodoItem[] = []
@Trace filter: string = 'all'
// 对应MobX的@computed
get filteredTodos(): TodoItem[] {
switch (this.filter) {
case 'done':
return this.todos.filter((t: TodoItem) => t.done)
case 'pending':
return this.todos.filter((t: TodoItem) => !t.done)
default:
return this.todos
}
}
get pendingCount(): number {
return this.todos.filter((t: TodoItem) => !t.done).length
}
// 对应MobX的@action
addTodo(title: string) {
this.todos = [...this.todos, { id: Date.now(), title: title, done: false }]
}
toggleTodo(id: number) {
this.todos = this.todos.map((t: TodoItem) => {
if (t.id === id) {
return { ...t, done: !t.done }
}
return t
})
}
setFilter(filter: string) {
this.filter = filter
}
}
// 页面使用
@Entry
@Component
struct TodoPage {
@Local store: TodoStore = new TodoStore()
@State newTodoTitle: string = ''
build() {
Column() {
// 统计信息
Row() {
Text(`待完成: ${this.store.pendingCount}`)
.fontSize(14)
.fontColor('#666666')
Blank()
// 筛选按钮
Row() {
Button('全部')
.fontSize(12)
.backgroundColor(this.store.filter === 'all' ? '#0A59F7' : '#E0E0E0')
.fontColor(this.store.filter === 'all' ? Color.White : '#333333')
.onClick(() => this.store.setFilter('all'))
Button('待办')
.fontSize(12)
.backgroundColor(this.store.filter === 'pending' ? '#0A59F7' : '#E0E0E0')
.fontColor(this.store.filter === 'pending' ? Color.White : '#333333')
.margin({ left: 8 })
.onClick(() => this.store.setFilter('pending'))
Button('已完成')
.fontSize(12)
.backgroundColor(this.store.filter === 'done' ? '#0A59F7' : '#E0E0E0')
.fontColor(this.store.filter === 'done' ? Color.White : '#333333')
.margin({ left: 8 })
.onClick(() => this.store.setFilter('done'))
}
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 12 })
// 输入框
Row() {
TextInput({ placeholder: '添加新待办...' })
.layoutWeight(1)
.onChange((value: string) => {
this.newTodoTitle = value
})
Button('添加')
.margin({ left: 8 })
.onClick(() => {
if (this.newTodoTitle.trim()) {
this.store.addTodo(this.newTodoTitle.trim())
this.newTodoTitle = ''
}
})
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 12 })
// 列表
List() {
ForEach(this.store.filteredTodos, (todo: TodoItem) => {
ListItem() {
Row() {
Checkbox()
.select(todo.done)
.onChange((checked: boolean) => {
this.store.toggleTodo(todo.id)
})
Text(todo.title)
.fontSize(16)
.decoration({
type: todo.done ? TextDecorationType.LineThrough : TextDecorationType.None
})
.margin({ left: 12 })
.layoutWeight(1)
}
.width('100%')
.padding(12)
}
}, (todo: TodoItem) => todo.id.toString())
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
}
MobX的makeAutoObservable对应ArkTS的@ObservedV2 + @Trace。@computed对应getter方法。@action对应普通方法。迁移非常直接。
完整示例:全局状态管理(替代Redux的Store)
Redux最核心的理念是"单一状态树"。ArkTS用AppStorage实现同样的效果。
// 鸿蒙: AppStorage替代Redux Store
// 第一步:定义全局状态
@ObservedV2
export class AppStore {
@Trace userInfo: UserInfo | null = null
@Trace token: string = ''
@Trace theme: string = 'light'
@Trace networkStatus: string = 'online'
// 计算属性
get isLoggedIn(): boolean {
return this.token !== '' && this.userInfo !== null
}
// 登录
async login(username: string, password: string) {
try {
let response = await http.createHttp().request('https://api.example.com/login', {
method: http.RequestMethod.POST,
extraData: JSON.stringify({ username, password })
})
if (response.responseCode === 200) {
let data = JSON.parse(response.result as string)
this.token = data.token
this.userInfo = data.user
}
} catch (e) {
console.error('登录失败: ' + (e as Error).message)
}
}
// 退出
logout() {
this.token = ''
this.userInfo = null
}
// 切换主题
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light'
}
}
// 第二步:在EntryAbility中初始化AppStorage
export default class EntryAbility extends UIAbility {
onCreate(want, launchParam) {
// 初始化全局Store
AppStorage.setOrCreate('appStore', new AppStore())
}
onWindowStageCreate(windowStage) {
windowStage.loadContent('pages/Index')
}
}
// 第三步:在组件中使用
@Entry
@Component
struct IndexPage {
// 从AppStorage获取全局状态
@StorageLink('appStore') appStore: AppStore = new AppStore()
build() {
Column() {
if (this.appStore.isLoggedIn) {
// 已登录
Text(`欢迎, ${this.appStore.userInfo?.name}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Button('退出登录')
.margin({ top: 20 })
.onClick(() => this.appStore.logout())
} else {
// 未登录
this.LoginForm()
}
// 主题切换
Button(`切换到${this.appStore.theme === 'light' ? '深色' : '浅色'}模式`)
.margin({ top: 20 })
.onClick(() => this.appStore.toggleTheme())
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(16)
}
@Builder
LoginForm() {
Column() {
TextInput({ placeholder: '用户名' })
.width('100%')
.margin({ bottom: 12 })
TextInput({ placeholder: '密码' })
.type(InputType.Password)
.width('100%')
.margin({ bottom: 20 })
Button('登录')
.width('100%')
.onClick(() => this.appStore.login('admin', 'password'))
}
.width('100%')
.padding(16)
}
}
// 其他页面也能访问同一个Store
@Component
export struct ProfilePage {
@StorageLink('appStore') appStore: AppStore = new AppStore()
build() {
Column() {
Text(this.appStore.userInfo?.name ?? '未登录')
.fontSize(24)
}
}
}
AppStorage就是ArkTS版的Redux Store。@StorageLink相当于useSelector+useDispatch,既能读又能写。@StorageProp相当于只读的useSelector。
踩坑与注意事项
坑1:@State只观测一层
ArkTS的@State默认只做浅层观测。如果你修改嵌套对象的属性,UI不会更新。
// ❌ 错误:修改嵌套属性不会触发更新
@State user: UserInfo = { name: '张三', address: { city: '北京' } }
updateCity() {
this.user.address.city = '上海' // UI不会更新!
}
// ✅ 正确:重新赋值整个对象
updateCity() {
this.user = { ...this.user, address: { ...this.user.address, city: '上海' } }
}
// ✅ 更好:用@ObservedV2 + @Trace做深度观测
@ObservedV2
class UserInfo {
@Trace name: string = ''
@Trace address: Address = new Address()
}
@ObservedV2
class Address {
@Trace city: string = ''
}
坑2:@Prop是单向的,修改不会回传父组件
从Redux/MobX过来的人容易犯这个错——以为@Prop和@Link一样是双向的。
// ❌ 错误:@Prop修改不会影响父组件
@Component
struct ChildComponent {
@Prop count: number = 0 // 单向!
build() {
Button('点击: ' + this.count)
.onClick(() => {
this.count++ // 只修改本地副本,父组件不知道
})
}
}
// ✅ 正确:需要双向同步用@Link
@Component
struct ChildComponent {
@Link count: number // 双向!
build() {
Button('点击: ' + this.count)
.onClick(() => {
this.count++ // 父组件也会更新
})
}
}
坑3:@Watch的回调时机
@Watch在状态变化后同步触发,不是异步的。如果你在@Watch回调里修改其他状态,可能会触发连锁更新。
@State price: number = 0
@State quantity: number = 0
@State total: number = 0
// ⚠️ 注意:@Watch回调是同步的
@Watch('onPriceChange')
price: number = 0
onPriceChange() {
this.total = this.price * this.quantity // 可能触发多次计算
}
// ✅ 更好:用getter替代@Watch
get total(): number {
return this.price * this.quantity
}
坑4:AppStorage的初始化时机
AppStorage必须在UIAbility的onCreate中初始化,不能在组件中延迟初始化。否则组件可能在AppStorage还没准备好时就尝试读取,导致空值错误。
坑5:状态更新批处理
和React的setState批处理不同,ArkTS的@State每次赋值都会触发更新。如果你需要一次修改多个状态,用@ObservedV2 + @Trace,它会在一个微任务中批处理所有更新。
HarmonyOS 6适配说明
HarmonyOS 6在状态管理上有重大升级:
-
@ObservedV2 + @Trace:替代了旧的@Observed + @Watch,深度观测不再需要手动配置。这是HarmonyOS 6最重要的状态管理改进。
-
@Local装饰器:替代@State用于组件内部状态,语义更清晰——“这是组件本地状态,不对外暴露”。
-
@Provider/@Consumer:跨层级状态传递更优雅,不需要一层层@Prop/@Link传递。
-
状态更新性能优化:@Trace的更新粒度更细,只更新真正变化的部分,不再整棵组件树刷新。
-
调试工具增强:DevEco Studio 6新增了状态调试面板,可以实时查看@State/@Trace的值变化历史。
总结
状态管理迁移的核心是理解ArkTS的"状态所有权"模型——谁定义状态,谁拥有状态,谁可以修改状态。
| 维度 | 学习难度 | 使用频率 | 重要程度 |
|---|---|---|---|
| @State/@Local | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| @Prop/@Link | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| @ObservedV2/@Trace | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| AppStorage | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| @Provide/@Consume | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| @Watch | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
从Redux/MobX迁移到ArkTS,最大的收获是代码量减少。你不再需要写Action类型、Reducer函数、Selector函数——直接修改属性就行。但最大的风险是状态管理变得"隐形",你需要更自觉地维护状态的单一数据源原则。
- 点赞
- 收藏
- 关注作者
评论(0)