HarmonyOS APP<玩转React>开源教程四:状态管理基础

第4次:状态管理基础

状态管理是构建交互式应用的核心。本次课程将学习 ArkUI 提供的各种状态装饰器,理解数据如何在组件间流动,并实现应用的主题切换功能。


学习目标

  • 理解状态与 UI 的关系
  • 掌握 @State 状态装饰器
  • 学会 @Prop 单向数据传递
  • 理解 @Link 双向数据绑定
  • 掌握 @StorageLink 应用级状态
  • 实现深色/浅色主题切换功能

4.1 状态与 UI 的关系

什么是状态?

状态是组件中可变的数据,当状态改变时,UI 会自动更新。

typescript 复制代码
@Component
struct Counter {
  @State count: number = 0;  // 这就是状态

  build() {
    Column() {
      Text(`计数: ${this.count}`)  // UI 依赖状态
      Button('+1')
        .onClick(() => {
          this.count++;  // 修改状态 → UI 自动更新
        })
    }
  }
}

状态驱动 UI 更新

复制代码
用户操作 → 修改状态 → 框架检测变化 → 重新执行 build() → 更新 UI

状态管理的层级

层级 装饰器 作用范围
组件内 @State 当前组件
父子组件 @Prop / @Link 父子组件间
跨组件 @Provide / @Consume 祖先与后代
应用级 @StorageLink / @StorageProp 整个应用

4.2 @State 状态装饰器

基本用法

@State 用于声明组件内部的可变状态:

typescript 复制代码
@Component
struct MyComponent {
  // 声明状态变量
  @State message: string = 'Hello';
  @State count: number = 0;
  @State isVisible: boolean = true;
  @State items: string[] = ['A', 'B', 'C'];

  build() {
    Column() {
      Text(this.message)
      Text(`Count: ${this.count}`)

      Button('修改')
        .onClick(() => {
          this.message = 'World';  // 修改状态
          this.count++;
        })
    }
  }
}

@State 的特点

  1. 必须初始化:声明时必须赋初始值
  2. 私有性:只能在组件内部访问和修改
  3. 响应式:值改变时自动触发 UI 更新

支持的数据类型

typescript 复制代码
@Component
struct StateTypes {
  // 基本类型
  @State str: string = '';
  @State num: number = 0;
  @State bool: boolean = false;

  // 对象类型
  @State user: User = { name: '张三', age: 25 };

  // 数组类型
  @State list: number[] = [1, 2, 3];

  // 枚举类型
  @State status: Status = Status.Pending;

  build() {
    // ...
  }
}

对象和数组的更新

typescript 复制代码
@Component
struct ObjectState {
  @State user: { name: string; age: number } = { name: '张三', age: 25 };
  @State items: string[] = ['A', 'B'];

  build() {
    Column() {
      Text(`${this.user.name}, ${this.user.age}岁`)

      Button('修改对象')
        .onClick(() => {
          // ✅ 正确:整体替换
          this.user = { name: '李四', age: 30 };

          // ✅ 正确:修改属性(会触发更新)
          this.user.age = 26;
        })

      Button('修改数组')
        .onClick(() => {
          // ✅ 正确:使用数组方法
          this.items.push('C');

          // ✅ 正确:整体替换
          this.items = [...this.items, 'D'];
        })
    }
  }
}

4.3 @Prop 单向数据传递

什么是 @Prop?

@Prop 用于接收父组件传递的数据,实现单向数据流(父 → 子)。

typescript 复制代码
// 子组件
@Component
struct ChildComponent {
  @Prop title: string;  // 接收父组件数据
  @Prop count: number;

  build() {
    Column() {
      Text(this.title)
      Text(`Count: ${this.count}`)
    }
  }
}

// 父组件
@Component
struct ParentComponent {
  @State parentTitle: string = '父组件标题';
  @State parentCount: number = 0;

  build() {
    Column() {
      // 传递数据给子组件
      ChildComponent({
        title: this.parentTitle,
        count: this.parentCount
      })

      Button('修改')
        .onClick(() => {
          this.parentTitle = '新标题';
          this.parentCount++;
        })
    }
  }
}

@Prop 的特点

  1. 单向绑定:父组件数据变化会同步到子组件
  2. 本地副本:子组件持有数据的副本
  3. 子组件可修改:但不会影响父组件
typescript 复制代码
@Component
struct Child {
  @Prop value: number;

  build() {
    Column() {
      Text(`Value: ${this.value}`)
      Button('子组件修改')
        .onClick(() => {
          this.value++;  // 只修改本地副本,不影响父组件
        })
    }
  }
}

实际应用示例

typescript 复制代码
// 模块卡片组件
@Component
struct ModuleCard {
  @Prop icon: string;
  @Prop title: string;
  @Prop description: string;
  @Prop progress: number;

  build() {
    Column() {
      Text(this.icon).fontSize(32)
      Text(this.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
      Text(this.description)
        .fontSize(12)
        .fontColor('#666')
      Progress({ value: this.progress, total: 100 })
        .width('100%')
    }
    .padding(16)
    .backgroundColor('#fff')
    .borderRadius(12)
  }
}

// 使用
@Entry
@Component
struct ModuleList {
  @State modules: Array<{icon: string; title: string; desc: string; progress: number}> = [
    { icon: '⚛️', title: 'React 简介', desc: '了解 React', progress: 100 },
    { icon: '🛠️', title: '环境搭建', desc: '配置开发环境', progress: 50 },
  ];

  build() {
    Column({ space: 12 }) {
      ForEach(this.modules, (item) => {
        ModuleCard({
          icon: item.icon,
          title: item.title,
          description: item.desc,
          progress: item.progress
        })
      })
    }
  }
}

什么是 @Link?

@Link 实现父子组件间的双向数据绑定,子组件的修改会同步到父组件。

typescript 复制代码
// 子组件
@Component
struct Counter {
  @Link count: number;  // 双向绑定

  build() {
    Row({ space: 12 }) {
      Button('-')
        .onClick(() => this.count--)
      Text(`${this.count}`)
        .fontSize(20)
      Button('+')
        .onClick(() => this.count++)  // 修改会同步到父组件
    }
  }
}

// 父组件
@Entry
@Component
struct Parent {
  @State totalCount: number = 0;

  build() {
    Column({ space: 20 }) {
      Text(`父组件显示: ${this.totalCount}`)
        .fontSize(24)

      // 使用 $ 符号传递引用
      Counter({ count: $totalCount })
    }
  }
}
  1. 双向同步:父子组件数据实时同步
  2. 引用传递 :使用 $ 符号传递状态引用
  3. 必须初始化:父组件必须传递 @State 变量
特性 @Prop @Link
数据流向 单向(父→子) 双向
传递方式 值传递 引用传递($)
子组件修改 不影响父组件 同步到父组件
使用场景 展示数据 表单、计数器等

实际应用:表单组件

typescript 复制代码
// 输入框组件
@Component
struct FormInput {
  @Link value: string;
  @Prop label: string;
  @Prop placeholder: string;

  build() {
    Column() {
      Text(this.label)
        .fontSize(14)
        .fontColor('#333')
        .margin({ bottom: 8 })

      TextInput({ placeholder: this.placeholder, text: this.value })
        .onChange((value: string) => {
          this.value = value;  // 双向绑定
        })
        .height(44)
        .borderRadius(8)
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
  }
}

// 使用
@Entry
@Component
struct LoginForm {
  @State username: string = '';
  @State password: string = '';

  build() {
    Column({ space: 16 }) {
      FormInput({
        value: $username,
        label: '用户名',
        placeholder: '请输入用户名'
      })

      FormInput({
        value: $password,
        label: '密码',
        placeholder: '请输入密码'
      })

      Button('登录')
        .onClick(() => {
          console.log(`用户名: ${this.username}, 密码: ${this.password}`);
        })
    }
    .padding(20)
  }
}

什么是应用级状态?

应用级状态是跨页面、跨组件共享的全局状态,使用 AppStorage 管理。

AppStorage 基础

typescript 复制代码
// 初始化应用级状态
AppStorage.setOrCreate('isDarkMode', false);
AppStorage.setOrCreate('username', '');
AppStorage.setOrCreate('token', '');

// 读取状态
let isDark = AppStorage.get<boolean>('isDarkMode');

// 修改状态
AppStorage.set('isDarkMode', true);

@StorageLink 创建与 AppStorage 的双向绑定:

typescript 复制代码
@Entry
@Component
struct SettingsPage {
  // 与 AppStorage 中的 'isDarkMode' 双向绑定
  @StorageLink('isDarkMode') isDarkMode: boolean = false;

  build() {
    Column() {
      Row() {
        Text('深色模式')
        Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
          .onChange((isOn: boolean) => {
            this.isDarkMode = isOn;  // 自动同步到 AppStorage
          })
      }

      // 根据主题显示不同样式
      Text('当前主题: ' + (this.isDarkMode ? '深色' : '浅色'))
        .fontColor(this.isDarkMode ? '#fff' : '#000')
    }
    .backgroundColor(this.isDarkMode ? '#1a1a2e' : '#f8f9fa')
  }
}

@StorageProp 装饰器

@StorageProp 创建单向绑定(只读):

typescript 复制代码
@Component
struct DisplayComponent {
  // 只读绑定,组件内修改不会同步到 AppStorage
  @StorageProp('username') username: string = '';

  build() {
    Text(`欢迎, ${this.username}`)
  }
}
特性 @StorageLink @StorageProp
绑定方式 双向 单向
组件修改 同步到 AppStorage 不同步
使用场景 需要修改全局状态 只需读取

4.6 实操:实现主题切换功能

现在,让我们为应用实现完整的深色/浅色主题切换功能。

步骤 1:创建 ThemeUtil.ets

entry/src/main/ets/common/ 目录下创建 ThemeUtil.ets

typescript 复制代码
/**
 * 主题工具类
 * 管理应用深浅主题切换
 */

/**
 * 主题模式枚举
 */
export enum ThemeMode {
  AUTO = 'auto',
  LIGHT = 'light',
  DARK = 'dark'
}

/**
 * 浅色主题颜色
 */
export const LightTheme = {
  background: '#f8f9fa',
  cardBackground: '#ffffff',
  textPrimary: '#1a1a2e',
  textSecondary: '#495057',
  primary: '#61DAFB',
  divider: '#e9ecef'
};

/**
 * 深色主题颜色
 */
export const DarkTheme = {
  background: '#1a1a2e',
  cardBackground: '#282c34',
  textPrimary: '#ffffff',
  textSecondary: '#d1d5db',
  primary: '#61DAFB',
  divider: '#3d3d5c'
};

/**
 * 初始化主题
 */
export function initTheme(): void {
  // 初始化 AppStorage 中的主题状态
  AppStorage.setOrCreate('isDarkMode', false);
  AppStorage.setOrCreate('themeMode', ThemeMode.LIGHT);
  console.info('[ThemeUtil] Theme initialized');
}

/**
 * 切换主题
 */
export function toggleTheme(): void {
  const currentIsDark = AppStorage.get<boolean>('isDarkMode') ?? false;
  const newIsDark = !currentIsDark;

  AppStorage.set('isDarkMode', newIsDark);
  AppStorage.set('themeMode', newIsDark ? ThemeMode.DARK : ThemeMode.LIGHT);

  console.info(`[ThemeUtil] Theme toggled to: ${newIsDark ? 'dark' : 'light'}`);
}

/**
 * 获取当前主题颜色
 */
export function getThemeColors(isDarkMode: boolean) {
  return isDarkMode ? DarkTheme : LightTheme;
}

步骤 2:更新 Index.ets

更新首页,添加主题支持:

typescript 复制代码
/**
 * React 学习教程 App - 首页(支持主题切换)
 * 第4次课程实操代码
 */
import { initTheme, toggleTheme, LightTheme, DarkTheme } from '../common/ThemeUtil';

@Entry
@Component
struct Index {
  @State currentTab: number = 0;
  @StorageLink('isDarkMode') isDarkMode: boolean = false;

  // 获取当前主题颜色
  get theme() {
    return this.isDarkMode ? DarkTheme : LightTheme;
  }

  aboutToAppear(): void {
    initTheme();
  }

  build() {
    Column() {
      // 顶部标题栏
      this.HeaderBar()

      // 主内容区
      this.MainContent()

      // 底部导航栏
      this.BottomNavigation()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.theme.background)
  }

  @Builder
  HeaderBar() {
    Row() {
      Text('⚛️')
        .fontSize(28)

      Text('React 学习教程')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.theme.textPrimary)
        .margin({ left: 8 })

      Blank()

      // 主题切换按钮
      Text(this.isDarkMode ? '🌙' : '☀️')
        .fontSize(24)
        .onClick(() => {
          toggleTheme();
        })

      Text('🔍')
        .fontSize(24)
        .margin({ left: 16 })
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor(this.theme.cardBackground)
  }

  @Builder
  MainContent() {
    Scroll() {
      Column() {
        this.HeroBanner()
        this.QuickAccess()
        this.RecommendedModules()
      }
    }
    .layoutWeight(1)
    .scrollBar(BarState.Off)
  }

  @Builder
  HeroBanner() {
    Column() {
      Text('开始你的 React 之旅')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ffffff')

      Text('由浅入深,系统掌握 React')
        .fontSize(14)
        .fontColor('rgba(255,255,255,0.9)')
        .margin({ top: 8 })

      Row() {
        this.StatItem('0', '已完成')
        this.Divider()
        this.StatItem('35', '总课程')
        this.Divider()
        this.StatItem('0', '连续天数')
      }
      .width('100%')
      .margin({ top: 24 })

      Row() {
        Text('📝 每日一题')
          .fontSize(14)
          .fontColor('#ffffff')
        Blank()
        Text('挑战 →')
          .fontSize(14)
          .fontColor('rgba(255,255,255,0.9)')
      }
      .width('100%')
      .padding(12)
      .margin({ top: 16 })
      .backgroundColor('rgba(255,255,255,0.15)')
      .borderRadius(12)
    }
    .width('100%')
    .padding(20)
    .linearGradient({
      angle: 135,
      colors: [['#61DAFB', 0], ['#21a0c4', 1]]
    })
    .borderRadius({ bottomLeft: 24, bottomRight: 24 })
  }

  @Builder
  StatItem(value: string, label: string) {
    Column() {
      Text(value)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ffffff')
      Text(label)
        .fontSize(12)
        .fontColor('rgba(255,255,255,0.9)')
        .margin({ top: 4 })
    }
    .layoutWeight(1)
  }

  @Builder
  Divider() {
    Column()
      .width(1)
      .height(40)
      .backgroundColor('rgba(255,255,255,0.3)')
  }

  @Builder
  QuickAccess() {
    Column() {
      Text('快捷入口')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.theme.textPrimary)
        .width('100%')

      Row({ space: 12 }) {
        this.QuickAccessItem('🎯', '面试题库', '海量面试真题')
        this.QuickAccessItem('💻', '在线编程', '实战代码练习')
        this.QuickAccessItem('📦', '成品下载', '11个示例项目')
      }
      .width('100%')
      .margin({ top: 12 })
    }
    .width('100%')
    .padding(16)
  }

  @Builder
  QuickAccessItem(icon: string, title: string, desc: string) {
    Column() {
      Text(icon)
        .fontSize(32)
      Text(title)
        .fontSize(13)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.theme.textPrimary)
        .margin({ top: 6 })
      Text(desc)
        .fontSize(11)
        .fontColor(this.theme.textSecondary)
        .margin({ top: 2 })
    }
    .layoutWeight(1)
    .padding(16)
    .backgroundColor(this.theme.cardBackground)
    .borderRadius(16)
    .shadow({
      radius: 8,
      color: this.isDarkMode ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.05)',
      offsetY: 2
    })
  }

  @Builder
  RecommendedModules() {
    Column() {
      Row() {
        Text('推荐模块')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.theme.textPrimary)
        Blank()
        Text('查看全部 →')
          .fontSize(14)
          .fontColor(this.theme.primary)
      }
      .width('100%')

      Scroll() {
        Row({ space: 12 }) {
          this.ModuleCard('⚛️', 'React 简介', '入门', '#51cf66')
          this.ModuleCard('🛠️', '环境搭建', '入门', '#51cf66')
          this.ModuleCard('🧩', '组件基础', '基础', '#339af0')
          this.ModuleCard('🎯', '事件与渲染', '基础', '#339af0')
          this.ModuleCard('🪝', 'Hooks 基础', '进阶', '#ff922b')
        }
        .padding({ right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .margin({ top: 12 })
    }
    .width('100%')
    .padding({ left: 16, top: 8, bottom: 20 })
  }

  @Builder
  ModuleCard(icon: string, title: string, level: string, color: string) {
    Column() {
      Row() {
        Text(icon)
          .fontSize(24)
        Blank()
        Text(level)
          .fontSize(10)
          .fontColor('#ffffff')
          .backgroundColor(color)
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .borderRadius(8)
      }
      .width('100%')

      Text(title)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.theme.textPrimary)
        .margin({ top: 8 })

      Text('3 课时 · 30分钟')
        .fontSize(11)
        .fontColor(this.theme.textSecondary)
        .margin({ top: 4 })
    }
    .width(140)
    .padding(12)
    .backgroundColor(this.theme.cardBackground)
    .borderRadius(16)
    .shadow({
      radius: 8,
      color: this.isDarkMode ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.08)',
      offsetY: 4
    })
  }

  @Builder
  BottomNavigation() {
    Row() {
      this.NavItem('🏠', '首页', 0)
      this.NavItem('📚', '课程', 1)
      this.NavItem('📖', '源码', 2)
      this.NavItem('🌟', '项目', 3)
      this.NavItem('👤', '我的', 4)
    }
    .width('100%')
    .height(60)
    .backgroundColor(this.theme.cardBackground)
    .border({ width: { top: 1 }, color: this.theme.divider })
  }

  @Builder
  NavItem(icon: string, label: string, index: number) {
    Column() {
      Text(icon)
        .fontSize(24)
      Text(label)
        .fontSize(12)
        .fontColor(this.currentTab === index ? this.theme.primary : this.theme.textSecondary)
        .margin({ top: 4 })
    }
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.currentTab = index;
    })
  }
}

步骤 3:运行测试

  1. 运行应用
  2. 点击顶部的 ☀️/🌙 图标
  3. 观察整个应用的颜色变化

预期效果

浅色模式

  • 背景:浅灰色 (#f8f9fa)
  • 卡片:白色 (#ffffff)
  • 文字:深色 (#1a1a2e)

深色模式

  • 背景:深蓝色 (#1a1a2e)
  • 卡片:深灰色 (#282c34)
  • 文字:白色 (#ffffff)

本次课程小结

通过本次课程,你已经:

✅ 理解了状态与 UI 的关系

✅ 掌握了 @State 组件内部状态管理

✅ 学会了 @Prop 单向数据传递

✅ 理解了 @Link 双向数据绑定

✅ 掌握了 @StorageLink 应用级状态

✅ 实现了完整的深色/浅色主题切换功能


课后练习

  1. 添加主题持久化:使用 Preferences 保存用户的主题选择

  2. 创建设置页面:添加一个设置页面,包含主题切换开关

  3. 扩展主题:添加更多主题颜色(如蓝色主题、绿色主题)


下次预告

第5次:项目架构设计

我们将学习如何设计一个可维护的项目架构:

  • 分层架构设计原则
  • 目录结构规划
  • 常量管理
  • 搭建完整项目骨架

良好的架构是项目成功的基础!

相关推荐
十六年开源服务商2 小时前
2026年WordPress多语言插件定制开发深度指南
开源
前端不太难2 小时前
90% 的鸿蒙 App,没有真正的依赖管理
华为·状态模式·harmonyos
十六年开源服务商2 小时前
2026开源CMS网站插件怎么做
开源
研究点啥好呢3 小时前
每日GitHub热门项目推荐 | 2026年3月9日(补充)
ai·开源·github·openclaw
weixin_446260853 小时前
提升开发效率的神器!快速选择编码上下文 — React Grab
前端·react.js·前端框架
Csvn4 小时前
使用 React Hooks 优化组件性能的 5 个技巧
前端·javascript·react.js
张一凡934 小时前
告别 Redux 的繁琐,试试这个基于类模型的 React 状态管理库:easy-model
前端·react.js
opbr4 小时前
Vite 插件实战:如何优雅地将构建时间注入到 HTML 中?
前端·开源