这几天在做 React Native 项目的鸿蒙适配,遇到的最头疼的问题不是组件渲染,也不是路由跳转,而是全局状态。登录态、用户信息、主题设置、购物车,这些状态在 iOS 和 Android 上用 Redux 很顺手,但搬到鸿蒙上总感觉有点别扭。后来我把核心状态全部换成了useContext + useReducer这套组合,反而觉得一身轻松。这不是说 Redux 不好,而是对于 RN 鸿蒙这种还比较年轻的适配链路,越少的外部依赖、越清晰的数据流,就越容易排坑。这篇文章就记录一下我这次适配中的完整思路和代码,包括踩过的几个坑,尤其是启动白屏那件事。适合正在做 RN 鸿蒙化、或者想在鸿蒙 App 里统一管理状态的开发者参考。如果你只是想了解一下鸿蒙开发里状态管理怎么写,也可以看,代码量不大,重点是思路。
1. 先搞清楚:鸿蒙上的 React Native 到底是怎么跑的
1.1 不是套壳浏览器,而是 ArkUI 原生渲染
React Native 跑在鸿蒙上,不是包了一层 WebView 的 H5 页面。目前开源社区的主流做法是通过 RNOH(React Native OpenHarmony)这类适配层,把 JS 里的 React 组件映射成 ArkUI 原生组件。比如 View 对应 ArkUI 的布局容器,Text 对应文本组件,ScrollView 对应滚动容器。JS 只负责业务逻辑和 UI 描述,真正渲染到屏幕上的是鸿蒙原生组件。这意味着状态管理的每一次变更,最终要跨过 JS 和原生之间的桥,触发 ArkUI 的更新。桥的效率和批量更新机制,直接决定你在鸿蒙上看到的渲染表现。明白这一点,就能理解为什么有些状态更新在 iOS 上秒开,在鸿蒙上却像卡了一下。
另外,千万别把 RN 鸿蒙当成浏览器来调试。你不能用浏览器那套 DOM 思维理解 Component,很多 CSS 属性也不完全等价。ArkUI 的布局模型有自己的规则,比如 Flex 在鸿蒙上更接近 ArkUI 的 Flex 布局,RelativeContainer 又是另一种定位思路。这些布局差异在状态管理里的影响,主要是让“哪些组件会被重新渲染”变得比 Web 上更难预测。不过核心的状态流还是一样的:状态在 Provider 里,事件走 dispatch,界面消费状态。所以只要把状态管理这一层做干净,后面切折叠屏、平板、元服务场景都会省心很多。
1.2 HarmonyOS NEXT 环境下的跨平台现实约束
现在很多鸿蒙设备上的新版本系统不再直接把安卓 APK 作为首选安装包格式,这意味着原来靠安卓包跑起来的方案行不通了。很多团队是第一次真正要为鸿蒙单独起一条构建链。RN 的优势是复用业务代码,但生态里的第三方原生库并不全都兼容。状态管理库如 Redux 大多是纯 JS,理论上没有原生依赖,但它们的周边生态,比如 Redux Persist 用到的 AsyncStorage,必须要有鸿蒙原生实现,否则在真机上就会报错。如果你的 Redux 中间件里还挂了原生模块,那鸿蒙构建失败的风险会直线上升。
所以我在选型时给自己定了几条硬标准:第一,必须纯 JS 或只依赖最基础的原生能力;第二,状态变更路径足够短,方便在鸿蒙上定位问题;第三,团队里新同学能快速上手。useReducer + useContext 全部来自 React 自身,不引入任何额外依赖,第一、第二条天然满足。第三条可能有点反常规,但只要把 reducer 的写法规范好,比 Redux 那一套 action/reducer/selector 概念还要简单。当然,项目大了以后可以再套一层 zustand,但起步阶段,用 React 内置方案最省心。
1.3 迁移时的状态管理选型标准
总结一下我的评判维度,方便你在自己的项目里做决定。不需要完全照搬,但要关注这几个点:原生依赖数量、数据流复杂度、调试成本、热更新兼容性、团队熟悉度。在纯 JS 的 React 项目里,Redux 和 Zustand 都很好;但放到鸿蒙适配链路里,状态管理库本身不是瓶颈,瓶颈往往在它依赖的原生插件、持久化方案和中间件上。所以迁移的第一步不是急着改代码,而是先盘点现有状态层里哪些是纯 JS、哪些碰了原生模块。碰过原生模块的,要么找到鸿蒙实现,要么就必须换掉。
2. 为什么全局状态偏偏选了 useContext + useReducer
2.1 对比 Redux / MobX / Zustand,我为什么不直接用成熟库
很多同事问我,现在 Zustand 那么轻,为什么不用?我的回答是:在普通 RN 项目里我确实会用 Zustand,但在鸿蒙适配初期,我倾向于把第三方依赖降到最低。下面这张表是我测过的方案对比,供你参考。
| 方案 | 纯 JS? | 鸿蒙适配风险 | 状态更新粒度 | 学习成本 | 我的评价 |
|---|---|---|---|---|---|
| Redux | 是,但周边生态复杂 | 中等,中间件和持久化可能踩坑 | 需要手动订阅,粒度可控制 | 较高 | 团队熟悉的话问题不大,但鸿蒙排障成本高 |
| MobX | 是,但依赖 Proxy 特性 | 中等,需要确认 Hermes 引擎兼容性 | 细粒度自动追踪 | 中 | 没有明显优势,反而多一层魔法 |
| Zustand | 是 | 低,依赖少 | 细粒度,支持选择器 | 低 | 后续可以引入,但初期没必要 |
| Context + Reducer | 是,可以做到零依赖 | 极低 | 整个 Context 消费方刷新 | 低 | 起步阶段首选,后面可演进 |
我最后选了最后一种,不是因为别的,而是在鸿蒙化早期,一个团队最怕的就是“打包失败找不出原因”。Context + Reducer 没有魔法,错误堆栈直接指向组件本身。它唯一的缺点是状态更新粒度不够细,但这个问题可以通过拆分 Context 来解决,后面第 4.4 节会专门讲。
2.2 Context 负责读,Reducer 负责写,职责刚好互补
useContext 解决的是“怎么把状态安全地送到任意深度的子组件”,useReducer 解决的是“怎么让状态的修改变得可预测、可回溯”。你可以把 Context 想象成公司公告栏,状态挂在公告栏上,谁想看自己去看;Reducer 则是审批窗口,所有修改都得填单子,不能直接改公告栏上的字。useReducer返回[state, dispatch],state 用于读,dispatch 用于写。这样一来,App 中所有和用户相关的状态修改都会走到同一个 reducer 函数,不会散落得到处都是。
这个特性在多端开发里特别有用。如果 iOS 上行为正常但鸿蒙上不对,我只需要检查 action 和 reducer,而不是在十几个组件里翻 setState。reducer是纯函数,传入相同的 state 和 action 一定会得到相同的 nextState,这在复现 bug 时是巨大的优势。我在鸿蒙真机上遇到过几次“偶发性状态不对”,最后都是靠 reducer 的确定性,把操作序列打印出来,一步步定位到问题。
2.3 什么状态适合放,什么状态千万别放
用户信息、登录态、全局配置、购物车数量这种跨页面、跨组件都要读的状态,适合放 Context。输入框正在输入的内容、列表滚动位置、当前弹窗开关这种局部状态,千万别放。Context 本质上是发布订阅,只要 value 引用变化,所有消费者都要重新协调一次。高频变化的状态放进去就是性能灾难。我之前把表单正在输入的内容放进 Context,结果每个键盘敲击都触发全页面刷新,鸿蒙上尤其明显,因为中间还隔着一层 ArkUI 更新。
正确做法是只放跨页面、跨组件都需要的低频状态。这也是 useReducer 喜欢的场景:事件少、影响大。比如用户登录、退出登录、修改昵称、更新头像,一天内发生的次数很有限,但影响面很大,非常适合通过 reducer 统一管理。如果你发现某个状态每个组件都在改、每秒钟都在变,那它大概率不应该放在全局。
3. 在 React Native 鸿蒙项目里落地 useContext + useReducer
3.1 工程准备与依赖
建议先用官方模板起一个全新 RN 工程,再把鸿蒙适配层接进来。我实际项目中是先让 iOS/Android 跑通,再执行鸿蒙适配命令。特别注意 RN 版本和鸿蒙适配库的版本要严格对齐,否则会出现各种奇怪的白屏。初始化命令大致长这样,包名和版本号以你当前使用的鸿蒙脚手架为准,不要硬抄:
# 创建工程 npx react-native init RNHarmonyDemo cd RNHarmonyDemo npm install # 接入鸿蒙适配层,具体包名见你的适配方案文档 npm install your-harmony-adapter-package装完依赖后,需要检查工程里有没有harmony目录,以及metro.config.js是否正确引用了鸿蒙平台。我见过不少“启动白屏”其实是因为 Metro 没把鸿蒙入口打进 bundle,而不是状态管理的问题。如果你用的是官方社区模板,跟着 README 走一般不会错。这里我想强调的是:状态管理是纯 JS 层的事,但它的运行环境依赖鸿蒙适配层的稳定。适配层版本不一致,后面的 useContext 再怎么写都是白搭。
3.2 封装全局 StoreProvider,代码直接可用
我习惯把 Context 文件和业务状态分开。下面这个UserProvider只负责用户登录态,放在src/store/UserProvider.js。为了让鸿蒙端能从本地恢复登录态,我直接在 Provider 里接入了持久化读取;如果你的鸿蒙工程还没有适配某个 AsyncStorage,就换成文档里推荐的鸿蒙本地存储模块,逻辑是一样的。
import React, { createContext, useContext, useEffect, useMemo, useReducer, } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; const UserContext = createContext(null); const initialState = { user: null, token: null, loading: true, }; function userReducer(state, action) { switch (action.type) { case 'LOGIN_SUCCESS': return { ...state, user: action.payload.user, token: action.payload.token, loading: false, }; case 'LOGOUT': return { ...state, user: null, token: null, loading: false, }; case 'SET_LOADING': return { ...state, loading: action.payload, }; default: return state; } } const STORAGE_KEY = 'user_session'; export function UserProvider({ children }) { const [state, dispatch] = useReducer(userReducer, initialState); useEffect(() => { async function hydrate() { try { const raw = await AsyncStorage.getItem(STORAGE_KEY); if (raw) { const data = JSON.parse(raw); dispatch({ type: 'LOGIN_SUCCESS', payload: data }); } else { dispatch({ type: 'SET_LOADING', payload: false }); } } catch (error) { console.warn('读取本地登录态失败', error); dispatch({ type: 'SET_LOADING', payload: false }); } } hydrate(); }, []); const value = useMemo(() => ({ state, dispatch }), [state]); return <UserContext.Provider value={value}>{children}</UserContext.Provider>; } export function useUser() { const ctx = useContext(UserContext); if (!ctx) { throw new Error('useUser 必须在 UserProvider 内使用'); } return ctx; }这里有几个细节要说明。useMemo非常重要,如果没有它,UserProvider 每次重新渲染都会生成新的 value,导致所有消费 UserContext 的子组件全部重渲染。加上useMemo后,只有state真的变化时,value 才变化。useUser这个自定义 hook 的守卫也很实用,在 React Native 中一般不会出错,但如果你不小心在 Provider 外面用了,它会直接抛一个明确的错误,比“undefined 读不到”好排查得多。
AsyncStorage只是一个占位示例。鸿蒙适配版如果还没有这个包,请使用你手头适配方案里推荐的本地存储模块。持久化这一层在鸿蒙上很容易踩坑,因为不同适配方案给出的 API 可能不一样,但只要把getItem和setItem两个方法替换掉,上面的 reducer 和 Context 逻辑不需要改。
3.3 在组件里消费状态:一个完整的登录/首页例子
登录页只需要发事件,不需要读用户状态,所以代码里只取出dispatch。这样写不是为了性能,而是让职责更清晰:登录页只管把登录结果交出去,首页才负责展示用户信息。
import React, { useState } from 'react'; import { Button, Text, TextInput, View } from 'react-native'; import { useUser } from '../store/UserProvider'; export default function LoginScreen({ navigation }) { const { dispatch } = useUser(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); async function handleLogin() { // 这里只做演示,真实的登录请求会返回 token const fakeUser = { id: '1', name: username }; const fakeToken = 'token_' + Date.now(); dispatch({ type: 'LOGIN_SUCCESS', payload: { user: fakeUser, token: fakeToken }, }); navigation.replace('Home'); } return ( <View style={{ padding: 20 }}> <Text>用户名</Text> <TextInput value={username} onChangeText={setUsername} /> <Text>密码</Text> <TextInput value={password} onChangeText={setPassword} secureTextEntry /> <Button title="登录" onPress={handleLogin} /> </View> ); }首页需要读取当前用户和 token,所以使用state。退出登录时发给 reducer 一个LOGOUTaction,状态会自动回到未登录,同时所有依赖useUser的页面会重新渲染。
import React from 'react'; import { Button, Text, View } from 'react-native'; import { useUser } from '../store/UserProvider'; export default function HomeScreen() { const { state, dispatch } = useUser(); return ( <View style={{ padding: 20 }}> <Text>欢迎回来,{state.user?.name || '未登录用户'}</Text> <Text>Token 前几位:{state.token?.slice(0, 6) ?? '无'}</Text> <Button title="退出登录" onPress={() => dispatch({ type: 'LOGOUT' })} /> </View> ); }如果页面很多,建议所有页面组件都从useUser()取状态,不要再通过 props 层层传递。RN 鸿蒙的组件树本就比 Web 深,状态经过 props 转发后,不仅代码丑,还容易漏更新。用 Context 直接取,至少能保证状态来源只有一个。
3.4 与 AsyncStorage 持久化联动,顺便解决启动白屏
很多启动白屏不是原生层没配好,而是 JS 首帧渲染时还在等异步数据。我在UserProvider里做了两件事:进入 useEffect 后立刻读本地缓存;读缓存期间loading保持 true。然后在应用入口处,loading 没结束时只渲染一个轻量 SplashView,这样 UI 不会出现“登录页闪一下”或者“白屏后内容才出现”的尴尬。
function Root() { const { state } = useUser(); if (state.loading) { return <SplashView />; } return ( <> {state.token ? <AppStack /> : <AuthStack />} </> ); }SplashView可以跟原生启动图保持一致,也可以是一个居中的 Loading 组件。关键是在 loading 未完成时不要渲染业务页面。有些人喜欢在根组件里等 token 回来再判断 NavigationContainer,但如果在 useEffect 里的异步回来前已经渲染了错误页面,就会闪屏。在鸿蒙上,由于 JS 初始化和原生桥建立更慢,闪屏问题会被放大。还有一点:hydrate函数里如果读取失败,一定要把loading置为 false,否则应用会永远卡在 SplashView。真机上测试时,我也遇到过本地存储里 JSON 解析失败的情况,那时候catch分支就是救命的。
4. 高频踩坑与排查技巧实录
4.1 改了状态 UI 不刷新,先检查 reducer 有没有返回新对象
最常见的“dispatch 后界面没变”,不是 Context 出了问题,而是 reducer 里没有返回新对象。比如有人图省事,写成state.user = null; return state;,这样 state 引用没有变,Provider 的 useMemo 依赖state就不会触发,所有子组件拿到的还是旧引用。Redux 也一样要求不可变更新,但 useReducer 因为没有强制插件,更容易犯这个错。解决办法很简单,每次更新都展开旧 state,生成一个新对象:return { ...state, user: null }。
第二种场景是组件没有真正通过useUser()消费 Context,而是从 props 拿了一个快照。如果父组件没有重新渲染,props 里的旧值会一直传下去。你在排查时先确认:当前组件到底是从 Context 读,还是从 props 读。RN 鸿蒙调试时,常见的一个现象是你在某个子组件里打印 state 是新的,但界面没变,这时候问题百分之百出在“界面读的不是这个 state”。
4.2 鸿蒙端启动白屏/首屏卡顿,怎么定位
我见过好几个项目,把初始化请求放在业务页面的 useEffect 中,iOS 上由于 RN 加载快,一闪而过,鸿蒙上直接白屏两三秒。后来把登录态恢复提前到最外层 Provider,白屏基本消失。核心原则是:首屏渲染必须不依赖网络请求和异步 IO。能同步读的本地缓存就在 Provider 里读;不能同步拿到的数据,在 UI 上要有明确占位;不要在组件函数体里直接做耗时操作,比如把一个大对象JSON.parse放在渲染路径上,会让鸿蒙端的首帧更慢。
如果白屏已经出现,建议先拆两段定位:第一段是原生启动图消失到 JS bundle 执行完成,这段时间看 Metro 日志和原生日志;第二段是 bundle 执行完成到业务首屏渲染,这段时间可以通过在 Root 组件里打 log 观察。很多情况下,第二段耗时都来自“异步状态还没回来就渲染了业务页面”。把loading状态加到全局,白屏问题就能从“症状”变成“可定位的流程”。
4.3 iOS 正常、鸿蒙上 Context 更新延迟,问题出在时序
有一次在鸿蒙真机上,登录成功后页面跳转了,但是首页的用户名还是旧的。iOS 和 Android 都正常。排查后发现,问题不在 Context,而是在 handleLogin 里 dispatch 后立刻navigation.replace('Home'),首页组件首次渲染时拿到的还是旧状态。原因是鸿蒙适配版的 state 批量提交时机比 iOS 晚一拍。解决办法是不要在同一事件里同时依赖“新状态去跳转”。先让状态更新落地,再让页面切换;或者在跳转后,目标页面先对state.user为空的情况做 loading 占位,而不是直接渲染空字符串。
这个坑很值得记下来。跨端开发时,React 本身的调度器在 iOS、Android、鸿蒙上的实际执行时序不完全一样。你在原生端看起来“正常”的代码,到了鸿蒙上可能就是竞态。只要 UI 上对“状态还没有更新”做了兜底,这类问题会少一大半。另外,InteractionManager.runAfterInteractions在鸿蒙端依然可用,如果你需要在状态更新后立刻做动画或者跳转,可以优先用这个 API。
4.4 Context value 里塞了太多东西导致全页面重渲染,用拆分 Context 解决
默认情况下,只要 reducer 里的任意字段变化,所有useUser()组件都会重新渲染。比如 token 更新和 user 头像更新会互相拖累。优化手段是拆分 Context:一个存 state,一个只存 dispatch。所有只需要发事件的组件只订阅 dispatch context,这样 state 变化不会触发它们。
const UserStateContext = createContext(null); const UserDispatchContext = createContext(null); export function UserProvider({ children }) { const [state, dispatch] = useReducer(userReducer, initialState); return ( <UserDispatchContext.Provider value={dispatch}> <UserStateContext.Provider value={state}> {children} </UserStateContext.Provider> </UserDispatchContext.Provider> ); } export function useUserState() { return useContext(UserStateContext); } export function useUserDispatch() { return useContext(UserDispatchContext); }dispatch 是稳定的引用,不会随状态变化,所以只订阅 dispatch 的组件永远不会因为 state 变化而重渲染。这是社区很常用的模式。如果你的业务页面很多,还可以按领域继续拆分,比如UserContext、SettingsContext、CartContext,各自维护自己的 reducer。鸿蒙端对渲染性能的敏感度比 iOS 高,赋值不变导致全页面刷新这种问题,越早优化越好。
4.5 Metro 缓存和热重载失效,先不要怀疑代码
鸿蒙适配版的热更新链路比 iOS/Android 长,经常会出现改了 reducer 后界面没变化的情况。第一反应不是怀疑代码,而是清缓存。执行一下:
npx react-native start --reset-cache然后重启鸿蒙应用。如果是真机,还要确认应用是否真的重新 bundle 了,有时旧 bundle 还在系统缓存里,需要卸载重装。这个问题我遇到不下五次,每次最后都是缓存。鸿蒙端的热重载目前还不能完美覆盖所有场景,尤其是你在改createContext和Provider结构的时候,热重载经常会失效。不要花大量时间找“代码哪里写错了”,先清缓存、重装,再回头查逻辑。
4.6 常见问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 启动白屏 | 异步登录态未完成就渲染业务页面 | Provider 内做持久化恢复,loading 时渲染 SplashView |
| dispatch 后 UI 不刷新 | reducer 返回了旧 state 引用 | 返回新对象,展开...state |
| 全页面重渲染 | Context value 中放太多状态 | 拆分 State/Dispatch Context,使用 useMemo |
| 鸿蒙上更新比 iOS 慢 | 批量提交时机差异、跳转过早 | 用 InteractionManager 延后跳转,目标页做占位 |
| 热重载不生效 | Metro 缓存 / bundle 未更新 | --reset-cache,卸载重装 |
| 页面崩溃且报 undefined | useUser 在 Provider 外使用 | 自定义 hook 里加守卫,抛出明确错误 |
这张表在我团队内部就是新人避坑手册,照着排查能省下不少时间。最后再强调一点:以上问题大多不是 Context/Reducer 本身的问题,而是接入鸿蒙后的时序和缓存问题。如果你在原生端正常、鸿蒙端异常,优先从这两点出发,不要一上来就重构状态方案。
5. 后续扩展:从“能用”到“好用”
5.1 想要 selector 粒度?先别手写,用拆分 Context 就够了
业务复杂以后,你可能希望某个组件只关心user.name,其他字段变了不刷新。这种需求在 Redux 里叫 selector,在 Context 里很难优雅地做到。我实际用的方案是优先拆分 Context,而不是手写 selector。因为手写 selector 在 React 18 里需要配合useSyncExternalStore才能做到真正的细粒度订阅,Context 本身的机制决定了你无法阻止所有消费组件的重渲染。如果单纯用 useMemo 包一层,看起来像是优化,实际上只是在计算层面上省了一点,重渲染还是会发生。
所以我的建议是:业务简单时直接拆分 Context;真到需要 selector 粒度,再用useSyncExternalStore或 Zustand 做外部 store。不要在这里造轮子。RN 鸿蒙适配期,稳定大于一切。
5.2 用 useEffect 做自动存档,注意防抖和竞态
持久化登录态可以不用每次手动调AsyncStorage.setItem,而是在 Provider 里监听 state 变化,自动写入。但要注意两点:第一,loading阶段不要写,否则会把未初始化的状态盖掉;第二,写操作要防抖,避免连续 dispatch 时频繁 IO。下面这段代码是我自己项目里的写法:
useEffect(() => { if (state.loading) return; const timer = setTimeout(() => { AsyncStorage.setItem( STORAGE_KEY, JSON.stringify({ user: state.user, token: state.token }) ); }, 300); return () => clearTimeout(timer); }, [state]);防抖的好处是,用户连续修改昵称、头像时,只有最后一次状态稳定后才会写入本地。坏处是,如果用户在写入完成前退出 App,可能会丢最后一次变化。解决思路是在 AppState 进入后台时,取消防抖立即写入。RN 鸿蒙也支持 AppState API,这块逻辑可以复用。请记住:不要在 reducer 里做副作用,reducer 必须是纯函数,本地持久化要放在 useEffect 或事件处理函数中。
5.3 团队协作规范:action 常量和 reducer 拆分
多人协作时,字符串 action type 很容易写错。建议抽一个actions.js,把 type 集中管理:
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; export const LOGOUT = 'LOGOUT'; export const SET_LOADING = 'SET_LOADING';然后在 reducer 里引用常量,而不是裸字符串。这样至少能借助 IDE 的自动补全,也能减少 typo。如果状态领域很多,还可以在 Provider 里再套一层,比如UserProvider、SettingsProvider互相独立,而不是写一个巨型 reducer。React 允许 Provider 嵌套,且多个领域互不干扰。我见过有人把所有状态都塞进一个 reducer,最后 action 类型膨胀,根本没法维护。按领域拆,是 Context + Reducer 这套方案能长期用下去的关键。
5.4 为鸿蒙特定场景做好准备:折叠屏、元服务、PC 多窗口
鸿蒙生态现在不只是手机,还有折叠屏、平板,甚至元服务和 PC 多窗口。这套状态管理方案的优势是跨窗口复用:LoginScreen、HomeScreen 只要布局适配了,状态流不用再改。我自己的项目已经在用 RelativeContainer 加 Flex 做不同屏幕密度下的布局切换,状态层完全感知不到屏幕变化,因为 Context 只保存业务数据,不保存布局状态。这一点非常重要。如果你把某个折叠屏的展开状态写进全局 Context,以后会有无数同步噩梦。相反,所有跟 UI 相关的状态用普通 useState 放在组件内部就好。
等后续要接鸿蒙元服务或者多窗口入口时,这套 Context/Reducer 可以直接平移到新入口,只需要在入口包一个 UserProvider 就行。我在适配过程中最深的体会是:状态管理没有银弹,越接近业务、越少依赖的方案,越能在新平台到来时站住脚。如果你也正在做 RN 鸿蒙化,建议先停下来盘一下全局状态,把登录态和用户信息用 useContext + useReducer 重构一遍,你会发现后续很多奇怪的渲染问题都从这里找到了根源。