State 与 JSX
这一课你将能做到
- 在 RN 中正确使用 useState / useEffect 管理组件状态和副作用,避开 stale closure 陷阱
- 用 Context 做跨层级数据共享,写出可复用的 AuthProvider
- 抽取自定义 Hook 复用请求逻辑,并处理 App 前后台切换这个 RN 特有场景
为什么要学
你已经会 React,这一课严格说「一半是复习」。但 RN 环境有三个特殊性:没有浏览器的 localStorage,状态想持久化要走原生存储;组件生命周期受 App 前后台切换影响(Web 里没有这个概念);长列表重渲染的代价比 Web 高得多。这一课就是把你会的 React 状态管理「翻译」到 RN 语境,并补上这三块短板。
人话:state 是组件的「短期记忆」——杀了进程就没了;props 是「父母给的交代」——只能从上传到下;Context 是「小区公告栏」——谁需要谁自己去看,不用挨家挨户送信。什么时候用哪个,是这一课的核心判断。
先建立图像
import { useState, useEffect } from 'react';
import { View, Text, Pressable } from 'react-native';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count 变了:', count);
}, [count]);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 32 }}>{count}</Text>
<Pressable onPress={() => setCount(c => c + 1)}>
<Text style={{ color: '#007bff', marginTop: 16 }}>点我 +1</Text>
</Pressable>
</View>
);
}
渲染结果:屏幕中央一个数字,点「点我 +1」数字递增,控制台同步打印。数据流的循环永远是:state 变了 → 组件重新渲染 → 界面更新。你改的不是界面,是数据;界面是数据的投影。
跟着做
步骤 1:useState 的四类常见状态
RN 项目里 90% 的 useState 逃不出这四类,先建立分类直觉:
const [text, setText] = useState(''); // ① 表单值:输入框、选择器
const [visible, setVisible] = useState(false); // ② UI 开关:弹窗、展开收起
const [list, setList] = useState([]); // ③ 列表数据:接口返回的数组
const [loading, setLoading] = useState(false); // ④ 加载标志:请求三态
更新数组和对象时必须返回新引用,直接改原对象 React 感知不到:
setList([...list, newItem]); // ✅ 新增:展开旧数组造新数组
setList(list.filter(i => i.id !== id)); // ✅ 删除:filter 返回新数组
setList(list.map(i => i.id === id ? {...i, done: true} : i)); // ✅ 改某一项
list.push(newItem); // ❌ 直接改原数组,界面不更新
步骤 2:setState 的异步性与函数式更新
// ❌ 连续调用基于旧值计算,三次只加了一次
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// ✅ 函数式更新:每次都拿到最新值,三次加三
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
setState 不立即改值,它只安排「下一次渲染用新值」。所以 setState 后立刻读 state 拿到的还是旧值——要看变化后的值,用 useEffect 监听。
步骤 3:useEffect 请求数据 + cleanup
useEffect(() => {
let cancelled = false;
fetch('https://jsonplaceholder.typicode.com/users')
.then(r => r.json())
.then(data => { if (!cancelled) setList(data); });
return () => { cancelled = true; }; // cleanup:组件卸载后不再 setState
}, []); // 空依赖 = 只在挂载时执行一次
依赖数组是 useEffect 的灵魂:空数组只跑一次,写上 [keyword] 则 keyword 每次变化都重新执行(先跑上一次的 cleanup)。effect 里用到的所有外部变量都要写进依赖,否则闭包拿到的是创建那一刻的旧值——这就是著名的 stale closure(过期闭包):
// ❌ keyword 变了 effect 不会重跑,搜的永远是旧关键词
useEffect(() => { search(keyword); }, []);
// ✅ 依赖写全
useEffect(() => { search(keyword); }, [keyword]);
步骤 4:Context 共享登录状态
登录用户这种「全局一份、到处要用」的数据,用 props 传五层是灾难。Context 的正解:
// context/AuthContext.js
import { createContext, useContext, useState, useMemo } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
// value 用 useMemo 固定引用,user 没变就不惊动消费组件
const value = useMemo(() => ({ user, setUser }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export const useAuth = () => useContext(AuthContext);
// App.js 根部包一层
<AuthProvider>
<NavigationContainer>...</NavigationContainer>
</AuthProvider>
// 任何深层组件一行取用,不用管自己在第几层
const { user, setUser } = useAuth();
步骤 5:自定义 Hook 复用请求逻辑
每个列表页都写一遍「loading + data + error」太蠢了,抽成 Hook:
// hooks/useFetch.js
import { useState, useEffect } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) { setData(d); setLoading(false); } })
.catch(e => { if (!cancelled) { setError(e); setLoading(false); } });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// 页面里一行搞定
const { data, loading, error } = useFetch('https://api.example.com/list');
自定义 Hook 就是「把 useState + useEffect 的组合打包成函数」,命名以 use 开头即可。这是 React 逻辑复用的主力手段。
步骤 6:AppState——监听前后台切换(RN 特有)
用户切到微信回了个消息再切回来,你可能需要刷新未读数。Web 没有这个场景,RN 用 AppState:
import { useEffect } from 'react';
import { AppState } from 'react-native';
useEffect(() => {
const sub = AppState.addEventListener('change', (state) => {
if (state === 'active') {
refetch(); // 从后台切回前台,刷新数据
}
});
return () => sub.remove(); // 别忘了清理
}, []);
state 的三个值:active(前台)、background(后台)、inactive(iOS 特有,来电/切多任务时的过渡态)。
讲清楚要点
- 状态放哪层的判断:只有本组件用 → useState;父子共享 → 提升到最近共同父组件;跨多层级(登录用户、主题)→ Context;服务端列表数据 → 第 7 课的请求层或专门数据库。
- 不可变更新是铁律:数组对象更新必须返回新引用,
push / 直接改属性都不会触发重渲染。
- Context 不是状态管理器:它只解决「传递」问题。value 一变,所有消费组件无脑重渲染——所以 value 要用 useMemo 固定,高频数据别放进来。
- 列表 item 用 React.memo:FlatList 的 renderItem 组件包一层 memo,父组件 state 变化就不会牵连几百行重渲染(第 11 课详讲)。
- 依赖数组写全:useEffect 用到的外部变量全写进依赖,否则就是 stale closure,数据永远是旧的。
- RN 没有 localStorage:state 只在内存里,杀掉 App 全没了。想持久化登录态要用 AsyncStorage/SecureStore(第 8 课)。
你可能会懵的地方
-
setState 之后立刻读,怎么还是旧值?
setState 是异步的,它只安排下一次渲染。要用最新值参与计算就用函数式更新 setCount(c => c + 1);要在变化后做事就用 useEffect 监听该值。
-
Context 和 Redux/Zustand 怎么选?
低频共享(用户信息、主题、语言)用 Context 足够;高频更新(表单联动、实时数据)或大型项目用 Zustand(轻量、按 selector 精确订阅);Redux 留给需要时间旅行、中间件审计的团队。
-
为什么子组件没用到 user,setUser 之后它也重渲染了?
它消费的 Context value 整个对象引用变了。三个解法:拆分 Context(user 和 setUser 分开)、value 用 useMemo 缓存、子组件包 memo。
-
useEffect 在开发模式跑了两次?
React 18 StrictMode 故意「挂载→卸载→再挂载」,帮你暴露没写 cleanup 的副作用。生产环境只跑一次。cleanup 写规范就不怕它。
-
组件卸载后继续 setState 会怎样?
React 18 不再报警告,但逻辑上仍是错的(比如给已离开的页面更新数据)。cleanup 标志位是防御性好习惯。
错法对照
| 错法 | 问题 | 正解 |
|---|
直接 list.push(x) | 不触发重渲染 | setList([...list, x]) 新数组 |
setCount(count + 1) 连续调 | 基于旧值,更新丢失 | setCount(c => c + 1) |
| useEffect 不写依赖数组 | 每次渲染都发请求 | 加 [] 或写全依赖 |
| 依赖漏写外部变量 | stale closure,拿到旧值 | 用到的全写进依赖 |
| Context value 每次新建对象 | 所有消费者无脑重渲染 | useMemo(() => ({user, setUser}), [user]) |
| props 传 5 层 | 中间组件被迫转发 | Context 或状态库 |
| 登录 token 存 useState | 杀 App 就没了 | 持久化到 SecureStore(第 8 课) |
| AppState 监听不 remove | 监听器泄漏,重复触发 | cleanup 里 sub.remove() |
卡点急救
- 界面不更新:第一嫌疑是直接改了 state 对象/数组没返回新引用;第二是 setState 写在了 render 函数体里(死循环)。
- useEffect 死循环:依赖里放了每次渲染都新建的对象/数组/函数——用 useMemo/useCallback 固定,或把常量移出组件。
- 数据总是「慢一拍」:stale closure 典型症状,检查 effect 依赖数组是不是漏了变量。
- Context 更新后某组件没变:确认它确实在 Provider 树下(Provider 包错层级是常见事故)。
面试常问
-
RN 状态管理和 Web 有何不同?
核心 API 完全一样(useState/useEffect/Context)。差异在场景:RN 要用 AppState 感知前后台切换并决定是否刷新;没有 localStorage,持久化必须走 AsyncStorage/SecureStore 等原生存储;长列表渲染成本高,memo 和引用稳定更关键。
-
Context 的优缺点?什么情况下不够用?
优点:免 props drilling、React 原生零依赖。缺点:value 变化导致所有消费者重渲染,无法按字段订阅。高频更新场景不够用时换 Zustand——它支持 selector 精确订阅,组件只在自己关心的切片变化时重渲染。
-
什么是 stale closure,怎么避免?
effect 或回调在创建时捕获了当时的 state 值,之后 state 更新但闭包里仍是旧值。解法:依赖数组写全让 effect 重建;回调里要最新值用函数式 setState 或 useRef 镜像一份。
自测三问
- 为什么直接
list.push() 界面不更新?新增、删除、修改某一项的正确写法各是什么?
- Context 适合存什么、不适合存什么?value 为什么要 useMemo?
- 如何监听 App 从后台切回前台并刷新数据?清理函数怎么写?
小结一句话
RN 的状态管理就是你已会的 React 那一套,加上三条 RN 补丁:AppState 感知前后台、持久化走原生存储、长列表记得 memo 和引用稳定。
延伸