5.1 React Native 组件内部状态


5.1 React Native 组件内部状态

本节摘要:React Native 的组件内部状态管理由 useState 与 useReducer 承担,跨组件共享由 Context API 解决。本节讲清三个工具的适用边界与用法:useState 管简单值、useReducer 管复杂更新逻辑、Context 管跨层共享,并解释"不可变更新"这一贯穿始终的原则。

读前必看(上)

阅读完本节,你应当能够:

  1. 用 useState 管理组件内部的简单状态。
  2. 用 useReducer 管理依赖前状态的多步更新逻辑。
  3. 用 Context API 实现跨组件共享数据。
  4. 说清"不可变更新"对触发界面刷新的必要性。

一、问题与直觉

从第 4 章的待办例子开始,你已经用过 useState 了。现在把镜头拉远:状态管理这件事,为什么会成为移动开发的核心话题?

想想一个真实应用:用户登录后,个人中心要显示昵称,购物车要显示数量,设置页要显示头像。这些界面分布在不同的组件里,但数据都来自"当前用户"这个全局状态。如果每个组件自己存一份,登录了要通知所有组件更新,数据不同步就会出各种诡异问题。

状态管理的本质是回答三个问题:数据存哪、谁有权限改、改了怎么通知界面。本节从最基础的三层讲起:useState(组件自己的数据)、useReducer(复杂更新逻辑)、Context(跨组件共享)。三个问题各有所答,先建立这个基础,再谈全局方案(5.3 节)。

1.1 先建立"状态分层"的心智模型

在动手写任何状态代码前,先在心里画一张分层图:最底层是组件内部状态(useState、useReducer),中间层是跨组件共享(Context),顶层是全局状态(Redux、Zustand,5.3 节)。每层解决一个规模的问题,从下往上"升级"。

大多数初学者犯的错是把所有状态一股脑塞进全局方案,结果把简单问题复杂化。正确的默认值是:能放组件内部就放组件内部,只有当"多个不相关的组件都要这份数据"时,才考虑向上提升。这个"从下往上"的默认路径,能让状态结构保持简单,也为后续维护留出空间。带着这个分层模型读本节,三个工具的位置就很清楚了。

2.1.1 useState 的细节:函数式更新与初始化

useState 有两个容易忽略的细节,提前掌握能少踩坑。

初始化函数。初始值计算较重时,传入函数而非直接传值,React 只在首次渲染时调用它:

const [data, setData] = useState(() => expensiveCompute());

函数式更新。当新状态依赖前一个状态时,用函数式写法避免闭包捕获旧值:

setCount(prev => prev + 1); // 推荐 setCount(count + 1); // 可能拿到过期值

这两条分别是"性能"与"正确性"的细节。函数式更新尤其重要——在快速连续点击、异步回调里,直接引用状态变量很容易拿到旧值。养成用函数式更新的习惯,是 React 开发者的分水岭之一。

二、核心原理

2.1 useState:最基础的状态

useState 是 React Hooks 的一部分,给函数组件添加局部状态:

import React, { useState } from 'react'; import { View, Text, Button } from 'react-native'; function Counter() { const [count, setCount] = useState(0); return ( <View> <Text>计数:{count}</Text> <Button title="加一" onPress={() => setCount(count + 1)} /> <Button title="减一" onPress={() => setCount(count - 1)} /> </View> ); }

useState 返回一个数组:第一个是当前状态值,第二个是更新函数。调用 setCount 会触发组件重新渲染,界面从新状态推导。它的特点是简单、局部、不共享——状态只属于声明它的组件。

2.2 useReducer:复杂更新逻辑的容器

当状态更新逻辑变复杂(多个操作、状态更新依赖前一个状态、多个关联值),useState 会显得散乱。useReducer 把更新逻辑集中到一个 reducer 函数里:

import React, { useReducer } from 'react'; import { View, Text, Button } from 'react-native'; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return initialState; default: throw new Error(); } } function ComplexCounter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <View> <Text>计数:{state.count}</Text> <Button title="加一" onPress={() => dispatch({ type: 'increment' })} /> <Button title="减一" onPress={() => dispatch({ type: 'decrement' })} /> <Button title="重置" onPress={() => dispatch({ type: 'reset' })} /> </View> ); }

reducer 是纯函数:给定相同输入(state 加 action),永远返回相同输出。这让状态更新可预测、可测试。dispatch 派发 action,reducer 计算新状态。它与 Redux 的 reducer 概念同源——现在学的这套,就是 5.3 节 Redux 的雏形。

2.3 Context:跨组件共享数据

当某个数据要被多个组件共享,而逐层传 props 又太繁琐时,用 Context。它像一条"数据暗管",挂在组件树上,子树里任何组件都能读取,无需逐层传递。

import React, { createContext, useContext, useState } from 'react'; const ThemeContext = createContext('light'); function App() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={theme}> <Toolbar /> <Button title="切换主题" onPress={() => setTheme(theme === 'light' ? 'dark' : 'light')} /> </ThemeContext.Provider> ); } function Toolbar() { const theme = useContext(ThemeContext); return <Text>当前主题:{theme}</Text>; }

三个步骤:createContext 创建上下文、Provider 提供值、useContext 消费值。Context 适合"全局但不频繁更新"的数据——用户信息、主题、语言偏好。频繁更新的数据用 Context 会引发大范围重渲染,这种场景应该考虑 5.3 节的方案。

三、工程实践要点

3.1 三个工具的适用边界

工具 适用场景 不适用场景
useState 组件内部的简单值 复杂更新逻辑、跨组件共享
useReducer 多操作、依赖前状态、多关联值 简单值(用 useState 更直接)
Context 跨层共享、全局低频数据 高频更新数据、组件内部状态

选择逻辑一句话:先想状态归属,再选工具。只属于自己用 useState 或 useReducer;要共享用 Context;共享又高频就上全局方案。

3.1.1 一个完整的"归属判断"演练

用第 4 章的待办应用做例子,把归属判断走一遍。输入框内容与待办列表是待办页面自己的状态,用 useState 放在页面组件里——它们不需要被别的页面共享。如果做一个"已完成待办数"的全局统计(首页要显示、设置页要显示),这份数据就需要提升:要么提升到共同父组件,要么放 Context。如果还要支持多页面实时同步,就该考虑 5.3 节的全局方案。

这个演练的关键是"每份数据都有归属"的思维方式。写代码前把数据列一张清单,标上"谁拥有、谁读取、谁修改",归属清晰,状态结构就稳定。很多大型应用的状态混乱,根源不是方案不够高级,而是归属从未被认真思考过。

3.2 不可变更新:触发的关键

这是 RN 状态管理最重要的原则:更新状态必须产生新引用,不能原地修改

// 错误:原地修改数组 const handleAdd = () => { items.push(newItem); // 不产生新引用,界面不更新 setItems(items); }; // 正确:产生新数组 const handleAdd = () => { setItems([...items, newItem]); // 新数组,触发渲染 };

React 靠引用比较判断状态是否变化。原地修改不产生新引用,React 感知不到变化。这个原则适用于对象、数组一切引用类型,是贯穿 Redux、Zustand 的通用规则。

3.2.1 不可变更新的实用技巧

处理对象与数组的不可变更新有几个高频技巧,直接记下来用:

需求 写法 说明
数组追加 setItems([...items, item]) 展开运算符
数组删除 setItems(items.filter(x => x.id !== id)) filter 生成新数组
数组修改一项 setItems(items.map(x => x.id === id ? {...x, done: true} : x)) map 加展开
对象改字段 setState(prev => ({...prev, name: newName})) 展开对象

这四行覆盖了日常 80% 的不可变更新需求。看到它们背后的共同点了吗?全都是"造新对象/数组,而不是改旧的"。这套技巧在 5.3 节的 Redux 里会原样出现——因为 Redux 的核心约束就是不可变更新。现在打好底子,后面学任何状态库都轻松。

⚠️ 常见坑:直接修改嵌套对象里的某个字段,以为 setState 会"检测到"。实际上 React 比较的是引用,嵌套字段变了但外层引用没变,依然不会重渲染。正确做法是逐层产生新对象。
💡 关键直觉:把状态当作"不可变快照"。每次更新都基于旧快照造一个新快照,旧的永远不改。这个思维一旦建立,绝大多数状态 bug 就消失了。

3.3 状态提升:当兄弟组件需要共享

两个兄弟组件需要同一份数据时,把状态"提升"到它们的共同父组件,通过 props 传下去。这是 React 的经典模式,也是 Context 的替代方案。选择标准:层级浅用状态提升(显式、类型安全),层级深用 Context(省去逐层传递)。

FAQ:RN 状态管理的高频问题

问:什么时候该从 useState 升级到 useReducer? 没有硬性标准,但有明显信号:状态更新逻辑超过三四行、多个操作共享同一套逻辑、更新依赖前一个状态且容易出错。出现这些信号就值得考虑 useReducer。它把逻辑集中到一处,可读性与可测性都更好。

问:Context 会导致性能问题吗? 会。Provider 的值一变,所有消费该 Context 的组件都会重新渲染。所以 Context 只适合"低频全局数据"(主题、用户信息、语言),不适合高频更新的数据(输入框内容、实时数据流)。高频场景用 5.3 节的状态库,它们有更细粒度的更新控制。

问:状态提升和 Context 怎么选? 核心看传递深度。一两个层级用状态提升,简单直接、类型安全;跨越五六层甚至更多用 Context,省去逐层透传的样板代码。另一个参考是"这份数据会被多少组件用"——用得多、散得开,Context 更划算。

问:组件卸载后 setState 会报错吗? 在异步回调里 setState 可能遇到"组件已卸载"的警告(React 18 之后改为静默忽略)。正确处理是在卸载时取消订阅或清理异步任务——这正好是 5.4 节生命周期要讲的内容。现在先留个印象:状态更新的时机与组件生命周期强相关。

3.4 状态设计的一个实用练习

把状态管理抽象成一套可操作的思考流程:列状态 → 定归属 → 选工具。先列出界面依赖的所有数据(字段与类型),再逐个决定归属(组件内、提升到父级、还是共享),最后按归属选择工具(useState、useReducer、Context)。拿"设置页面"举例:开关值属于设置页组件内(useState),当前用户信息是全局共享(Context 或全局库),主题设置被多个页面读取(提升或共享)。这个练习多做几次,状态设计就从"凭感觉"变成"走流程"。

3.4.1 一个被低估的习惯:状态注释

在状态定义处写一行注释,说明"这个状态是什么、谁在修改、什么时候变",是成本极低收益极高的习惯。它解决了状态管理的核心难题——状态来源不清晰。三个月后回看代码,注释告诉你"count 是购物车数量,加减按钮修改,结算后清零",比翻遍所有引用它的地方快得多。这个习惯在个人项目里帮助已经很大,在团队项目里几乎是必需品。

3.5 一个综合练习:完整走一遍状态流程

用一个"购物车"例子把本节的工具全串起来:商品列表(父组件数据,通过 props 传入)、数量加减(子组件内部状态 useState)、加入购物车(提升到父组件共享)、购物车弹窗(Context 共享或全局库)。这个例子横跨 useState、状态提升、Context 三个层级,做完一遍,你对"状态归属"的判断会从概念变成手感。练习时特别注意每层状态的边界——哪些必须共享、哪些保持局部,这是状态设计最见功力之处。

3.6 本节与后续的衔接

本节的三件套(useState、useReducer、Context)是 RN 状态管理的起点,也是第 5.3 节全局方案的地基。理解了这个基础,再去看 Redux 或 Zustand 的文档,你会发现它们只是"更结构化的 Context 加更严格的状态约束"。带着这个视角学全局方案,会少很多陌生感。反过来说,如果本节的基础还不牢(比如说不清 useState 与 useReducer 的边界),先回头巩固,别急着上全局库——地基不稳,上面盖什么都会晃。

本章回顾

  • useState:组件内部简单状态,局部、不共享。
  • useReducer:复杂更新逻辑集中管理,reducer 是纯函数,可预测可测试。
  • Context:跨层共享数据,三步走(创建、提供、消费)。
  • 适用边界:先想状态归属,再选工具;归属决定方案。
  • 不可变更新:更新必须产生新引用,原地修改不会触发渲染。
  • 状态提升:兄弟组件共享用父组件提升,层级深才用 Context。
  • 高频数据:Context 不适合高频更新,这种情况考虑全局方案。

组件内部状态的三件套已掌握,下一节看 Flutter 的对应方案——setState 与 InheritedWidget,原理相通,写法不同。


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