4.1 Hooks状态基础与Context共享


4.1 Hooks状态基础与Context共享

本节摘要:状态阶梯的第一级与第二级:组件内用 useState 与 useReducer 建模,跨层共享用 Context。本节给出「归属三问」作为放置决策的第一道闸,用状态机思维升级复杂状态的建模方式,并讲透 Context 的订阅穿透陷阱——Context 不是迷你全局库,用错层级会把「共享」变成「全树连坐重渲染」。学完本节,多数中小应用的状态问题已经够用,4.2 的状态库只在你真的需要时才登场。

归属三问:状态放哪的第一道闸

拿到一块状态先别写代码,问三个问题:谁读它?谁写它?它的生命周期跟谁走?三问全指向当前组件,就用 useState;读写跨越好几层组件、但变更频率低(主题、语言、登录态这类「设置型」数据),Context 合适;读写高频、全局、多源变更(购物车、播放器状态),那才是 4.2 状态库的领地。跳过这三问直接把一切塞进全局库,是最常见的过度设计——你会为「一个只属于弹窗的开关」付出全局订阅与调试的复杂度。

先把组件内的基础打牢。useState 的三个要点:更新函数支持传入函数完成「基于旧值算新值」(连续多次更新必须用它,否则闭包里的旧值会互相覆盖);状态要「最小化」——能推导出来的值不要存(全名等于姓加名,那就别存全名);对象状态更新要换引用而不是改字段,渲染对比靠引用判断。当一块状态的动作开始成组出现(添加、删除、勾选、清空),useState 会散成一盘散沙,此时升级到 useReducer:把所有变更收敛为「动作类型加载荷」的单一入口,状态迁移逻辑聚在一处可测可审。

购物车是 useReducer 的标准教材:

import React from 'react'; import { View, Text, Pressable, StyleSheet } from 'react-native'; // 状态迁移集中声明:每个动作一条路径,审查与测试都方便 function cartReducer(state, action) { switch (action.type) { case 'ADD_ITEM': { const exist = state.items.find(it => it.id === action.item.id); const items = exist ? state.items.map(it => it.id === action.item.id ? { ...it, count: it.count + 1 } : it) : [...state.items, { ...action.item, count: 1 }]; return { ...state, items }; } case 'REMOVE_ITEM': return { ...state, items: state.items.filter(it => it.id !== action.id) }; case 'CLEAR': return { ...state, items: [] }; default: return state; } } export function CartDemo() { const [cart, dispatch] = React.useReducer(cartReducer, { items: [] }); const total = cart.items.reduce((s, it) => s + it.price * it.count, 0); return ( <View style={styles.wrap}> <Pressable style={styles.btn} onPress={() => dispatch({ type: 'ADD_ITEM', item: { id: 'p1', name: '示例商品', price: 9.9 } })}> <Text style={styles.btnText}>加入示例商品</Text> </Pressable> <Text style={styles.total}>合计:{total.toFixed(2)} 元</Text> </View> ); } const styles = StyleSheet.create({ wrap: { padding: 16, gap: 12 }, btn: { backgroundColor: '#1a73e8', borderRadius: 8, padding: 10, alignItems: 'center' }, btnText: { color: '#fff' }, total: { fontSize: 15, color: '#2c3e50' }, });

用状态机视角看这段代码,购物车的取值空间与迁移路径一目了然。下图把「空车、有货、已清空」的迁移关系画了出来——当你的状态也开始有这种「图的形状」时,就是 useReducer 登场的信号。

Context:共享的近路,也是连坐的陷阱

属性是一层一层传的,当一块「设置型」状态要穿过五层组件到达叶子,中途三层根本不用它却被迫接力——这就是属性钻透。Context 提供近路:提供方在上游声明,消费方在任意下游直接订阅,中间层完全透明。主题色是教科书场景:

import React from 'react'; import { createContext, useContext, useState } from 'react'; import { View, Text, Pressable } from 'react-native'; const ThemeContext = createContext(null); export function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); // 注意:value 用对象字面量时,Provider 每次渲染都会生成新引用, // 导致所有消费者重渲染——用 memo 包一层或拆分 Context 可对冲 const value = React.useMemo(() => ({ theme, toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')) }), [theme]); return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; } export function useTheme() { return useContext(ThemeContext); } export function ThemeButton() { const { theme, toggle } = useTheme(); return ( <Pressable onPress={toggle}> <Text>当前:{theme === 'light' ? '浅色' : '深色'},点我切换</Text> </Pressable> ); }

它的陷阱与能力同源:Context 的订阅粒度是「提供方」——value 一变,所有消费者全部重渲染,不管它们用没用变化的那部分。两个对冲手法:其一,按变更频率拆分 Context,把「高频变的状态」与「低频变的设置」分家,别让登录名和实时位置住在同一个 Context 里;其二,用 memo 缓存 value 引用,让不涉及的变化不触发。但要诚实地说清边界:Context 服务于「低频共享」,如果你的状态每秒变好几次(表单实时校验、动画进度),连坐式重渲染会立刻在性能监视器上现形——那是状态库或状态下沉的领地。

完整案例:主题切换的双端细节

背景:应用要支持浅深主题切换,初期实现把主题对象放在最顶层组件的 state 里逐层传,穿过导航与列表后,改一处主题要动六个文件。操作:迁移到 Context 后文件数降为一;接着处理两端细节——状态栏前景色在两端是不同属性(iOS 看状态栏样式参数,Android 看背景亮度),消费主题的根布局里用平台分支分别设置;再对列表场景做针对性优化,把主题值 memo 化避免滚动时无谓重渲染。结果:切换主题全应用即时生效,列表滚动帧率不受切换影响,两端状态栏颜色同步变化。解读:这个案例一半是 Context 的正确使用,一半是「共享状态里藏着平台差异」的典型——状态是全局统一的,落地到土壤的接口却要分别翻译,这个模式在第 5 章导航与第 7 章桥接里会反复出现。变式:若产品后续要求「跟随系统主题」,监听两端系统的外观变化事件并同步进 Context 即可,架构不需要动。

本节要点回顾

  • 归属三问先行:谁读、谁写、生命周期跟谁走,三问之后再选容器;
  • useReducer 的信号:动作成组出现、状态迁移有图的形状时,从 useState 升级;
  • 状态最小化:能推导的值不存,对象更新换引用不改字段;
  • Context 是低频共享的近路:订阅粒度是提供方,高频状态放进去就是连坐重渲染;
  • 拆分与 memo 两个对冲:按变更频率分家、缓存 value 引用。

共享半径再往上扩,Context 就力不从心了。下一节把三家状态库请上台,正面对比心智模型与工程成本。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U