HarmonyOS开发:状态管理迁移——Redux/MobX到ArkTS状态

举报
Jack20 发表于 2026/06/28 21:05:25 2026/06/28
【摘要】 HarmonyOS开发:状态管理迁移——Redux/MobX到ArkTS状态📌 核心要点:ArkTS内置的状态管理系统比Redux/MobX更轻量,@State/@Link/@Trace替代了Store/Reducer/Action的样板代码,但你需要重新理解"谁拥有状态"这个核心问题。 背景与动机你写前端或者React Native,Redux用了三年,Action/Reducer/M...

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在状态管理上有重大升级:

  1. @ObservedV2 + @Trace:替代了旧的@Observed + @Watch,深度观测不再需要手动配置。这是HarmonyOS 6最重要的状态管理改进。

  2. @Local装饰器:替代@State用于组件内部状态,语义更清晰——“这是组件本地状态,不对外暴露”。

  3. @Provider/@Consumer:跨层级状态传递更优雅,不需要一层层@Prop/@Link传递。

  4. 状态更新性能优化:@Trace的更新粒度更细,只更新真正变化的部分,不再整棵组件树刷新。

  5. 调试工具增强:DevEco Studio 6新增了状态调试面板,可以实时查看@State/@Trace的值变化历史。

总结

状态管理迁移的核心是理解ArkTS的"状态所有权"模型——谁定义状态,谁拥有状态,谁可以修改状态。

维度 学习难度 使用频率 重要程度
@State/@Local ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
@Prop/@Link ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
@ObservedV2/@Trace ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
AppStorage ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
@Provide/@Consume ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
@Watch ⭐⭐ ⭐⭐⭐ ⭐⭐⭐

从Redux/MobX迁移到ArkTS,最大的收获是代码量减少。你不再需要写Action类型、Reducer函数、Selector函数——直接修改属性就行。但最大的风险是状态管理变得"隐形",你需要更自觉地维护状态的单一数据源原则。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。