
写这篇教程的时候我明显感觉到“玩转React”这个系列已经进入了最关键的阶段。前面几期讲了环境搭建、工程配置、基础组件和页面路由那些都属于“会用”的层面从组件化开发这一期开始才真正进入“设计”的层面。很多刚接触 HarmonyOS React 的朋友容易把注意力全放在语法和 API 上结果写出来的代码堆在几个巨大的文件里页面一多就改不动、测不了、查不清。组件化开发要解决的恰恰就是代码膨胀之后的可维护性、可复用性和可测试性问题。这篇文章我会从 React 的组件模型出发结合 HarmonyOS 应用的实际工程场景把组件拆分思路、通信方式、状态管理和常见的坑讲清楚适合已经能跑通 Hello World、想认真做项目的开发者。1. 为什么先聊组件化这不是方法论是工程底线1.1 当 HarmonyOS 应用涨到一定规模缺组件化是什么样的我先描述一个场景你大概率见过一个 HarmonyOS 应用的所有页面逻辑全部放在一个 Page 里一个文件两三千行UI 布局、业务数据处理、网络请求、状态变更全部揉在一起。刚开始功能少改起来还挺顺手等页面数到十来个以后你会发现一个需求改动要牵动三四个页面公共样式复制粘贴了几十份某个按钮的交互逻辑在好几个地方各写了一套最终结果是新同事不敢碰老代码老同事改完新需求又回归出旧 Bug。这就是不做组件化的必然代价。在 React 的世界里组件化不是一个可选项而是它的设计原点。React 把界面拆成一个个独立单元每个单元维护自己的逻辑和外观然后再组合成完整页面。HarmonyOS 侧的 ArkUI 虽然是声明式范式的但它本质上也存在 View/Component 层级的拆分逻辑如果你的 React 代码还要跑在鸿蒙的 Web 容器或者 RN for OpenHarmony 方案里那组件化更是避不开的核心约束。我在实际项目中见过很多团队就是因为一开始没有在组件边界上花心思后面为了把一个共用弹窗改成统一样式折腾了整整一个迭代。组件化开发的核心目标是让代码具备三个能力可复用、可替换、可独立演进。可复用好理解同一个卡片组件在首页用、在详情页也用写一次就够。可替换的意思是组件内部实现出了更好的方案或者要换一个视觉风格不会牵连外部调用方。可独立演进则意味着不同业务模块的团队可以并行开发互不阻塞只依靠约定好的接口通信。这三个能力落到工程上就是开发效率、交付质量和团队协作的保障。1.2 组件化和我们常说的“模块化”是一回事吗很多人会把组件化和模块化混在一起其实它们的粒度不一样。模块化更多是站在工程层面按功能域划分比如登录模块、支付模块、订单模块一个模块里可以有一堆页面、接口、工具函数而组件化是站在 UI 与交互层面指一个可独立渲染、拥有自己的状态与行为的界面片段。组件可以属于某个模块也可以跨模块复用。比如“价格标签”是一个组件“商品列表”是一个组件“商品卡片”也是一个组件它们可以被登录页、首页、订单页一起引用。React 里组件的最小单元既可以是函数也可以是类关键是它接收输入输出界面同时带有自己的生命周期。HarmonyOS 的 ArkUI 里的 Component 结构体其实也是类似思路你把 UI 片段和逻辑封装起来对外只暴露必要的属性。React 组件通过 JSX 描述视图ArkUI 组件通过结构体加装饰器描述视图虽然语法不同但分类的方式是一致的展示型组件和容器型组件。展示型组件负责渲染内部几乎没有业务逻辑容器型组件负责数据和状态再把数据分发下去。这个分类是我在实际开发中最先要求团队统一认识的。2. 看懂 React 组件模型再谈鸿蒙侧适配2.1 把组件当作函数Props 是入参State 是内部变量React 的组件化思想完全可以浓缩成一句话组件就是一个返回界面的函数。这句话看着简单但真正理解透的人不多。函数式组件接收 props 对象作为参数返回 JSX 元素当这个“函数”内部还需要记录变化的数据时就用 useState 声明内部状态。整个组件的行为都可以用“入参 内部状态 输出界面”来推导。这里有个新手经常绕不过来的点为什么 props 在子组件里不能直接改。因为 props 是父组件传给子组件的“单向数据流”它就像函数的入参函数内部改参数虽然能实现但会让逻辑变得不可预测尤其当多个地方都依赖这个值时一个子组件改了它其他兄弟组件就全乱了。我自己的习惯是props 只读要变更就通过回调函数通知上层。在 HarmonyOS 的 ArkUI 里Prop 和 Link 的设计其实也是这个思路Prop 是父传子的本地副本Link 是双向绑定但双向绑定用多了同样会造成数据流混乱能不用尽量不用。2.2 函数组件和类组件怎么选现在 React 官方推荐的是函数组件加 Hooks类组件虽然还存在但已经不作为新代码的首选。函数组件更接近“组件是函数”的本质心智负担低也没有 this 绑定那一堆坑。在类组件里你得时刻注意 this.setState 是异步的、多个 setState 会被合并、事件处理函数里 this 指向会丢这些细节在实际开发中非常容易踩坑。但类组件也有它存在的场景比如你维护的旧项目或某些依赖类组件生命周期的第三方库。我见过一些团队在 HarmonyOS 上做混合开发老代码全是类组件迁不动那就没必要强行重构类组件依然能优雅运行。在新的功能模块里用函数组件 Hooks 就好保持新旧隔离比一上来就推倒重来稳妥得多。函数组件里的 useEffect 可以模拟类组件的 componentDidMount、componentDidUpdate、componentWillUnmount它把分散的生命周期逻辑收敛到一个地方可读性反而更好。3. 在鸿蒙工程里落地组件化从目录到实现3.1 一个能直接抄的组件目录结构理论讲再多不如直接看工程怎么组织。这里分享一个我目前比较满意的目录结构它适用于 HarmonyOS React不管是 H5 侧还是 RN 侧的项目src/ ├── components/ # 全局通用组件库 │ ├── Button/ │ │ ├── index.tsx │ │ ├── style.scss │ │ └── types.ts │ ├── PriceTag/ │ │ ├── index.tsx │ │ └── style.scss │ └── ... ├── pages/ # 页面级容器组件 │ ├── Home/ │ │ ├── index.tsx │ │ ├── HomeViewModel.ts │ │ └── components/ # 页面私有组件 │ └── Detail/ ├── hooks/ # 自定义 Hooks │ ├── useFetch.ts │ └── useCountdown.ts ├── services/ # 接口请求和数据处理 ├── store/ # 全局状态库 └── utils/ # 纯工具函数components 下面放的是跨页面复用的通用组件按钮、弹窗、空状态、价格标签这些。pages 下面每个页面是一个目录页面内部如果还有拆分出来的子组件就放在该页面的 components 子目录里不要混到全局目录去。hooks 目录专门放自定义 Hooks它能帮你把状态逻辑从组件里抽出来复用。这个结构的核心逻辑是通用组件全局共享页面组件就近管理状态逻辑单独沉淀。3.2 确定组件拆分粒度的三条判断标准你肯定会问一个页面拆成几个组件合适组件拆太细层级深、props 传递链条拉得很长拆太粗等于没拆。我一般用三个标准判断第一看它是否承担独立职责。比如“展示商品价格”是一个职责“处理加购按钮的交互”又是另一个职责职责不同就值得拆开。第二看它是否可复用。同一个视觉块在两个页面以上出现就放进全局组件库。第三看它是否可能单独变化。如果多个模块的展示强耦合在一起其中一个变化可能会带动另一个一起变化那它们就不适合拆开。比如头像和用户名也许可以拆成两个组件但在绝大多数场景它们总是成对出现的那我更倾向保留一个 UserInfo 组件而不是拆成两个。拆分组件的最终目的是让“修改局部界面的成本”尽可能低。一个组件只做一件事改它的时候不需要关心其他部分你的心情会好很多。3.3 手写一个组件从 Props 定义到渲染这里我们写一个商品价格组件它会在首页列表、商品详情、购物车三个地方复用。先定义组件的 Propsinterface PriceTagProps { price: number; // 当前售价单位分 originPrice?: number; // 原价可选传了就展示划线价 currency?: string; // 货币符号默认 ¥ size?: small | medium | large; showPromotion?: boolean; // 是否展示促销角标 }组件实现就按函数组件的思路来const PriceTag ({ price, originPrice, currency ¥, size medium, showPromotion false, }: PriceTagProps) { const hasDiscount originPrice originPrice price; return ( div className{price-tag price-tag--${size}} span classNameprice-tag__current {currency} {(price / 100).toFixed(2)} /span {hasDiscount ( span classNameprice-tag__origin {currency} {(originPrice / 100).toFixed(2)} /span )} {showPromotion hasDiscount ( span classNameprice-tag__promotion限时优惠/span )} /div ); };这个组件看起来简单但它把所有可能变化的点都提炼成了 props价格单位统一用“分”是因为后端接口通常返回整数分避免浮点误差originPrice 用可选链和条件判断让组件能适配有折扣和无折扣两种场景size 控制样式层级。这三个点在拆其他组件时同样适用凡是数据可能变、展示可能变、场景可能变的地方都应该暴露成 props 而不是写死在组件内部。4. 组件之间怎么通信单向往下的模式4.1 父子通信props 向下回调向上React 组件通信的第一条铁律是单向数据流。父组件通过 props 把数据传给子组件子组件需要修改数据时不能直接改 props而是调用父组件通过 props 传下来的回调函数由父组件去更新。这个模式唯一的别扭之处是多层嵌套时中间组件要逐层转发回调但它的好处是数据流向清晰出了问题可以顺着调用链一路往上查不需要翻完整份代码才能定位是谁改的。举个例子一个购物车商品卡片组件点击“增加数量”按钮后需要通知父组件更新数量。子组件内部只负责调用 props 里的 onIncrease 方法不关心父组件怎么处理interface CartItemProps { item: CartItem; onIncrease: (id: string) void; onDecrease: (id: string) void; }父组件在回调里更新对应商品的数量状态然后重新把新数据通过 props 传给子组件界面自然刷新。这个流程绕了一圈但每一步都可预测。HarmonyOS 的 ArkUI 里也有类似的关系Prop 实现单向同步Link 实现双向同步Event 提供回调能力。和 React 的 props 回调模型一一对应。如果你是在 HarmonyOS 侧写 React 页面再嵌入原生组件要特别注意原生组件和 React 组件之间的消息桥接通常由原生侧通过事件分发React 侧通过监听回调来对接这个边界就应该被封装成一个个专用组件不要让业务组件直接感知原生事件。4.2 跨层通信状态提升与共享 Store当两个兄弟组件需要共享同一份数据时直接 props 传需要先传到父组件再分发下去层级一深就变得笨重。我的做法是先做状态提升也就是把共享状态放到它们最近的共同父组件里再通过 props 分发给子组件。只有状态提升解决不了例如两个跨模块页面之间共享数据时才引入全局状态库。全局状态库在 HarmonyOS React 场景里选型很灵活常见的 Zustand、Redux Toolkit、Jotai 都能用。我个人比较偏爱 Zustand模板代码少心智负担低不需要像 Redux 那样写 action、reducer、selector 一整套组件里直接调用 store 的 hook 就能读写状态。但无论选哪种都要记住全局状态里只放真正的全局数据比如用户登录信息、购物车数量、主题设置页面级的临时状态包括表单输入值、弹窗开关、列表筛选条件等就近放在组件内部或页面级的 ViewModel 里就够了全部塞全局 Store 不仅会让状态依赖一团乱麻还会带来不必要的组件刷新。4.3 跨层跨页通信里的 Context 与持久化跨层跨页通信除了全局 Store 之外React 的 Context 也是一个好工具。Context 主要用于“跨越多层组件传递同一份数据且中间组件不关心这份数据”的场景比如把主题色、当前语言、登录用户往下透传。用法也比较直接const UserContext createContextUser | null(null); App UserContext.Provider value{user} DeepComponent / /UserContext.Provider /AppDeepComponent 不需要通过每一层 props 接收 user直接 useContext 就能拿到。这个模式在 HarmonyOS 场景里非常好用因为它的主题、字体等全局配置往往是多个层级的组件共享的全靠中间层转发连维护的人都嫌烦。还需要注意有时候组件通信要跨越页面而页面切换后原有组件实例会被销毁单纯靠内存里的 state 是没法保留数据的。这时候就需要借助持久化存储HarmonyOS 侧可以用 Preferences 键值库或 SQLite 保存关键数据应用启动时再读取并初始化全局状态。比如用户购物车列表在离开页面、重启应用后要恢复这就是一个典型的“状态持久化 跨页通信”结合场景。5. 组件化进阶组合、复用与状态管理的组织方式5.1 用组合代替继承在 React 的世界里继承基本是被抛弃的。组件之间的复用关系更适合用组合来描述一个组件里嵌套另一个组件通过 children 属性传入任意子元素或者作为 props 传入一段渲染函数。举一个典型例子页面需要统一的外壳组件包括顶栏、安全区、底部占位。你可以写一个 PageContainerinterface PageContainerProps { title: string; showBackIcon?: boolean; rightSlot?: React.ReactNode; children: React.ReactNode; }然后在渲染时把 header 部分统一处理页面内容通过 children 注入。这样首页、详情页、个人中心只需要各自传入内容外壳统一顶部导航栏也天然保持一致。用 React 完成组合的方式很多children 嵌套、render prop、自定义 Hook 都可以原则只有一个复用逻辑时优先想组合不要想着建基类。5.2 抽离自定义 Hooks逻辑也能组件化组件化不只包括 UI 组件的复用逻辑的复用同样重要。自定义 Hook 的常见场景包括网络请求、计时器倒计时、地理位置获取、按键监听它们都有一个特点和 UI 无关但会在多个组件里用到。比如倒计时 Hook可以用在验证码按钮、秒杀提醒、订单支付倒计时const useCountdown (initialSeconds: number) { const [seconds, setSeconds] useState(initialSeconds); const [isRunning, setIsRunning] useState(false); useEffect(() { if (!isRunning || seconds 0) return; const timer setInterval(() { setSeconds((prev) { if (prev 1) { clearInterval(timer); setIsRunning(false); return 0; } return prev - 1; }); }, 1000); return () clearInterval(timer); // 组件卸载时清理定时器 }, [isRunning, seconds]); const start useCallback(() { setIsRunning(true); }, []); return { seconds, start }; };在使用这个 Hook 的组件里三行代码搞定倒计时逻辑。在 HarmonyOS 的鸿蒙原生侧逻辑复用通常靠工具类 Extract但 React 侧的自定义 Hook 可以承载更复杂的状态逻辑比如“请求中、请求成功、请求失败”三段状态的封装。这一点我认为是 React 组件化领先传统命令式 UI 开发的一大步。5.3 组件库的沉淀与规范项目做久了之后你会发现每个团队最后都会沉淀出一套自己的组件库。这些组件统一了设计规范、交互模式和交互文案也让新页面开发变成搭积木。组件库的沉淀有几个经验每个组件都要有完整的类型定义、默认值注释和文档示例组件在页面里出现第三次就值得提进全局组件库一次只升级一个组件的视觉规范然后全应用回归不要一次性改所有组件。我在团队里推进组件化时专门定了两条规矩。一是组件目录里必须放一个 types.ts把所有对外 props 的接口定义集中管理业务调用方只看这个文件就知道组件能干什么。二是组件的样式选择器统一加前缀避免和页面样式互相污染。这些看起来是小细节但组件越多这两条规则的收益越明显。6. 三种高频问题与排查实录6.1 组件渲染了但界面不更新这是 React 开发者最常遇到的问题。现象是状态明明变了界面却纹丝不动。大部分原因出在组件未触发重新渲染上。常见错误包括直接修改了 props 对象内部的某个字段比如 props.user.name xxx没有创建新对象在函数组件里使用 useState 时setState 传入的是同一个引用对象React 比较引用发现没变就跳过了更新在列表渲染时使用 index 作为 key结果列表顺序变化后组件状态错乱。排查建议是先看状态是否真的被修改了再看组件是否重新执行了 render。使用 React DevTools 的 Profiler 可以直观地找到哪些组件重新渲染了。如果确认状态修改了但组件没走 render检查是否在 useCallback / useMemo 的依赖数组里遗漏了变量这是隐蔽的“状态更新了但组件还是旧闭包”问题。6.2 列表组件渲染大列表掉帧在 HarmonyOS 的 Web 容器里渲染长列表性能问题比纯 Web 端更明显因为设备内存和浏览器内核调度都有瓶颈。优化手段最常见的就是数据虚拟化也就是只渲染可视区域内的 item。react-window 或 react-virtualized 都是成熟方案。如果你用的是 HarmonyOS 原生 ArkUI 列表也应该优先采用 LazyForEach 做懒加载这和 React 侧虚拟列表的思路完全一致。我做列表优化的时候还有一个心得列表项的组件要注意切割让每一行 item 组件尽量“薄”。列表的 state 尽量只保存在 item 组件自身不要让整个列表父组件因为滚动、选中等操作频繁更新。把“列表容器组件”和“列表项组件”的依赖解耦开能避免一个 item 变化就导致整列重渲染。6.3 页面白屏与组件初始化异常白屏问题的原因层级很多但最常见的是三类组件渲染前的数据没准备好比如接口失败后没有给默认空值直接访问了 undefined 的深层属性全局状态在页面初始化时还没加载完成或者是 WebView / RN 容器加载 React 包时发生了异常导致整个根组件未能挂载。排查白屏时我建议分四步走。第一步看控制台是否有 JS 报错有报错先修报错第二步检查入口文件里 createRoot 是否正常调用第三步看接口请求是否返回了预期数据组件初始化时对缺失数据有哪些兜底第四步查看资源加载情况确认 JS Bundle 文件没有在下载过程中被截断。如果你的应用在鸿蒙上出现白屏而浏览器里正常优先排查打包产物在原生容器中的加载路径和沙箱限制这是 HarmonyOS 侧特有问题。再补充一个平时很少人提的小技巧给每个页面组件设置 ErrorBoundary 错误边界当某个页面出现运行时错误时至少能渲染一个降级提示页面而不是全屏白屏。这个做法成本很低但对用户体验提升巨大。写在最后的实操体会我带着团队在 HarmonyOS 应用里做 React 组件化改造前后也踩了不少坑。回头看最重要的一件事不是学会一个具体 API而是建立一种“边界感”——什么时候该拆组件什么时候不该拆什么数据该放组件内部什么数据必须提升到上层哪些状态值得进全局 Store哪些状态放在本地就够了。边界划清楚了组件化的收益自然就出来了。还有一个心得组件化不是一蹴而就的事你不可能在第一天把所有组件规划完美更合适的做法是边开发边沉淀等一个组件在场景里出现了三次再把它抽成通用组件。这个节奏比一开始设计一个超级庞大的组件体系要靠谱得多。