【React Native娓娓道来】第4课 副作用与数据获取:useEffect、网络请求与加载状态
开篇:渲染之外的那些事
前三课我们反复强调一句话:
// 概念表达
视图 = 函数(状态) // 视图是状态经过函数计算出来的结果
组件是函数,接收状态,返回视图描述。状态变,函数重新执行,界面自动更新。
但真实应用不只是"根据状态显示界面"。打开页面要从服务器拉数据,提交表单要发请求,倒计时需要定时器,页面离开要取消未完成的请求。这些事情有一个共同点:它们不是"计算界面",而是"在界面之外做点什么"。
在 React Native 中,这类事情叫副作用。
这一课要解决的问题是:
副作用放在哪里?什么时候执行?做完之后,状态怎么更新,界面怎么跟着变?
一、渲染函数必须是纯的
1.1 什么是纯函数
纯函数满足两个条件:相同输入得到相同输出;不修改外部变量,不做副作用。
// JavaScript
function add(a, b) { // 定义纯函数 add
return a + b; // 返回两数之和
} // 结束 add
输入 1 和 2,永远返回 3。它不修改外部变量,也不发请求、不写文件。
1.2 为什么渲染必须纯
组件函数会被执行很多次。每次状态变化、props 变化,组件都会重新执行。如果渲染过程中做了副作用,会出问题。
// JavaScript + React Native
// 错误:渲染中发请求
function UserProfile() { // 定义组件
fetch('https://api.example.com/user'); // 渲染中发请求
return <Text>用户信息</Text>; // 返回界面描述
} // 结束组件
每次重新渲染都会发一次请求。状态一变、父组件一更新、热重载一次,请求就多一次。结果就是:请求满天飞,数据混乱,性能下降。
// JavaScript + React Native
// 错误:渲染中修改外部变量
let total = 0; // 外部变量
function Counter() { // 定义组件
total = total + 1; // 渲染中修改外部变量
return <Text>{total}</Text>; // 返回界面描述
} // 结束组件
每次渲染都改 total,界面变得不可预测。
所以,React Native 要求:渲染过程只做一件事——根据当前状态,计算并返回视图描述。
那么网络请求、定时器、订阅这些事放哪里?答案是:useEffect。
二、useEffect:副作用的归宿
2.1 基本用法
// JavaScript + React Native
import { useEffect } from 'react'; // 引入 useEffect Hook
import { Text } from 'react-native'; // 引入 Text 组件
function App() { // 定义函数组件 App
useEffect(() => { // 注册副作用
console.log('组件渲染完成了'); // 副作用内容
}); // 不传依赖数组,每次渲染后执行
return <Text>你好</Text>; // 返回界面描述
} // 结束 App
useEffect 接收一个函数,这个函数在组件渲染完成之后执行。注意顺序:
// 概念表达
渲染 -> 返回视图描述 -> 界面更新 -> 执行 useEffect // useEffect 在渲染完成后执行
不是渲染之前,也不是渲染之中,而是渲染之后。
2.2 三种依赖数组
第二个参数是依赖数组,决定副作用什么时候执行。
// JavaScript + React Native
useEffect(() => { // 副作用回调
console.log('每次渲染后都执行'); // 副作用内容
}); // 不传依赖数组:每次渲染后都执行
useEffect(() => { // 副作用回调
console.log('只在挂载后执行一次'); // 副作用内容
}, []); // 空数组:只在挂载后执行一次
useEffect(() => { // 副作用回调
console.log('count 变化时执行'); // 副作用内容
}, [count]); // 具体依赖:挂载后 + count 变化时
| 依赖数组 | 执行时机 |
|---|---|
| 不传 | 每次渲染后 |
[] | 只在挂载后一次 |
[a, b] | 挂载后,以及 a 或 b 变化时 |
大多数场景用的是后两种。
2.3 依赖数组比较的是什么
React 用 Object.is 比较依赖数组里每一项的上一次值和这一次值。
- 基本类型(数字、字符串、布尔值):比较值;
- 对象或数组:比较引用。
所以,依赖里放对象或数组,而每次渲染又都创建了新对象,useEffect 会每次执行:
// JavaScript + React Native
// 反例:每次渲染都创建新对象,导致 useEffect 每次执行
useEffect(() => { // 副作用回调
console.log('会频繁执行'); // 副作用内容
}, [{ id: 1 }]); // 每次渲染都创建新对象
正确做法是依赖具体的字段:
// JavaScript + React Native
useEffect(() => { // 副作用回调
console.log(user.id); // 使用 user.id
}, [user.id]); // 依赖具体的字段
2.4 一个完整的执行流程
// JavaScript + React Native
import { useState, useEffect } from 'react'; // 引入 useState 和 useEffect
import { View, Text, Button } from 'react-native'; // 引入 View、Text、Button
export default function App() { // 定义并默认导出 App
const [count, setCount] = useState(0); // 创建状态 count,初始 0
useEffect(() => { // 注册副作用
console.log(`count 现在是 ${count}`); // 打印当前 count
}, [count]); // 依赖 count
return ( // 返回界面描述
<View> // 外层容器
<Text>你点击了 {count} 次</Text> // 显示点击次数
<Button title="加一" onPress={() => setCount(count + 1)} /> // 点击加一
</View> // 结束 View
); // 结束 return
} // 结束 App
流程:
- 首次渲染,
count是 0; - 渲染完成后,
useEffect执行,打印"count 现在是 0"; - 点击按钮,
count变成 1; - 组件重新渲染;
- 因为
count变了,useEffect再次执行,打印"count 现在是 1"。
useEffect 里的代码不会阻塞渲染,它是渲染完成之后的"后续动作"。
三、清理函数:副作用也要善后
3.1 为什么需要清理
有些副作用需要善后:定时器要清除,订阅要取消,请求要中止。如果不管,可能导致内存泄漏、重复执行,或者在已卸载的组件上更新状态。
3.2 返回清理函数
useEffect 的回调可以返回一个函数,这个函数就是清理函数。
// JavaScript + React Native
import { useEffect } from 'react'; // 引入 useEffect
useEffect(() => { // 副作用回调
const timer = setInterval(() => { // 启动定时器
console.log('每秒执行一次'); // 每秒打印
}, 1000); // 间隔 1000 毫秒
return () => { // 返回清理函数
clearInterval(timer); // 清除定时器
console.log('定时器已清除'); // 打印清理日志
}; // 结束清理函数
}, []); // 空数组,只执行一次
执行顺序:
- 组件挂载后,启动定时器;
- 组件卸载前,执行清理函数,清除定时器。
如果依赖数组里有值,清理函数的执行时机是:
- 依赖变化导致副作用重新执行之前,先执行上一次的清理函数;
- 组件卸载前,执行最后一次清理函数。
// JavaScript + React Native
useEffect(() => { // 副作用回调
console.log(`进入 count = ${count}`); // 副作用开始
return () => { // 返回清理函数
console.log(`离开 count = ${count}`); // 副作用结束
}; // 结束清理函数
}, [count]); // 依赖 count
每次 count 变化,都会先"离开"旧值,再"进入"新值。
一句话理解:每次副作用开始之前,先把上一次的副作用收拾干净。
四、用 useEffect 获取数据
4.1 最简单的数据获取
// JavaScript + React Native
import { useState, useEffect } from 'react'; // 引入 useState 和 useEffect
import { View, Text, ActivityIndicator } from 'react-native'; // 引入 View、Text、ActivityIndicator
export default function App() { // 定义并默认导出 App
const [data, setData] = useState(null); // 创建状态 data,初始 null
const [loading, setLoading] = useState(true); // 创建状态 loading,初始 true
useEffect(() => { // 注册副作用
async function load() { // 定义异步加载函数
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 发请求
const json = await res.json(); // 解析 JSON
setData(json); // 更新 data
setLoading(false); // 关闭 loading
} // 结束 load
load(); // 调用 load
}, []); // 空数组,只执行一次
if (loading) { // 还在加载
return <ActivityIndicator />; // 显示加载指示器
} // 结束判断
return ( // 返回界面描述
<View> // 外层容器
<Text>{data.title}</Text> // 显示数据标题
</View> // 结束 View
); // 结束 return
} // 结束 App
流程:首次渲染 loading 为 true,显示加载指示器。渲染完成后 useEffect 执行,发请求。请求返回后更新 data 和 loading,组件重新渲染,显示数据。
4.2 为什么不能直接写 async
// JavaScript + React Native
// 错误示例
useEffect(async () => { // async 回调,不允许
const res = await fetch('https://api.example.com'); // 发请求
const json = await res.json(); // 解析 JSON
setData(json); // 更新数据
}, []); // 空数组
useEffect 的回调应该返回清理函数或 undefined。写成 async 会返回 Promise,React 无法识别,会给出警告。
正确做法是在回调里定义一个 async 函数,再调用它:
// JavaScript + React Native
useEffect(() => { // 普通回调
async function load() { // 内部定义 async 函数
const res = await fetch('https://api.example.com'); // 发请求
const json = await res.json(); // 解析 JSON
setData(json); // 更新数据
} // 结束 load
load(); // 调用 load
}, []); // 空数组
4.3 完整的三态:加载中、成功、失败
真实的数据获取通常有三种状态。用一个变量管理会乱,用三个更清楚:
// JavaScript + React Native
import { useState, useEffect } from 'react'; // 引入 useState 和 useEffect
import { View, Text, ActivityIndicator, Button, StyleSheet } from 'react-native'; // 引入需要的组件
export default function App() { // 定义并默认导出 App
const [data, setData] = useState(null); // 数据状态,初始 null
const [loading, setLoading] = useState(true); // 加载状态,初始 true
const [error, setError] = useState(null); // 错误状态,初始 null
const load = async () => { // 定义加载函数
try { // 开始 try
setLoading(true); // 打开 loading
setError(null); // 清空 error
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 发请求
if (!res.ok) throw new Error(`请求失败:${res.status}`); // 检查状态码
const json = await res.json(); // 解析 JSON
setData(json); // 更新 data
} catch (e) { // 捕获错误
setError(e.message); // 更新 error
} finally { // 收尾
setLoading(false); // 关闭 loading
} // 结束 try
}; // 结束 load
useEffect(() => { // 注册副作用
load(); // 首次加载
}, []); // 空数组
if (loading) { // 加载中
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<ActivityIndicator size="large" /> // 加载指示器
<Text>加载中...</Text> // 加载提示
</View> // 结束容器
); // 结束 return
} // 结束判断
if (error) { // 出错
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<Text>出错了:{error}</Text> // 错误信息
<Button title="重试" onPress={load} /> // 重试按钮
</View> // 结束容器
); // 结束 return
} // 结束判断
return ( // 成功
<View style={styles.center}> // 居中容器
<Text style={styles.title}>{data.title}</Text> // 显示标题
<Text>{data.body}</Text> // 显示正文
</View> // 结束容器
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, // 居中容器样式
title: { fontSize: 18, fontWeight: 'bold', marginBottom: 8 }, // 标题样式
}); // 结束样式表
这个例子展示了:data、loading、error 三态;try/catch/finally 处理成功、失败、收尾;res.ok 判断 HTTP 状态码;出错时提供重试按钮。
三态是数据获取的标准模式,几乎所有真实项目都会用到。
4.4 避免在已卸载组件上更新状态
如果用户在请求还没返回时离开了页面,请求返回后又调用了 setData,可能出问题。解决方法是:在 useEffect 里用一个标记,组件卸载时把它设为 true。
// JavaScript + React Native
useEffect(() => { // 注册副作用
let cancelled = false; // 定义取消标记
async function load() { // 定义异步加载函数
try { // 开始 try
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 发请求
const json = await res.json(); // 解析 JSON
if (!cancelled) setData(json); // 如果没卸载,更新 data
} catch (e) { // 捕获错误
if (!cancelled) setError(e.message); // 如果没卸载,更新 error
} finally { // 收尾
if (!cancelled) setLoading(false); // 如果没卸载,关闭 loading
} // 结束 try
} // 结束 load
load(); // 调用 load
return () => { // 返回清理函数
cancelled = true; // 组件卸载时设为 true
}; // 结束清理
}, []); // 空数组
组件卸载时,清理函数把 cancelled 设为 true。请求返回后,if (!cancelled) 为假,就不会再更新状态。
也可以使用 AbortController 直接取消请求:
// JavaScript + React Native
useEffect(() => { // 注册副作用
const controller = new AbortController(); // 创建控制器
async function load() { // 定义异步加载函数
try { // 开始 try
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1', { // 发请求
signal: controller.signal, // 传入信号
}); // 结束 fetch
const json = await res.json(); // 解析 JSON
setData(json); // 更新 data
} catch (e) { // 捕获错误
if (e.name !== 'AbortError') setError(e.message); // 如果不是取消导致的错误
} finally { // 收尾
setLoading(false); // 关闭 loading
} // 结束 try
} // 结束 load
load(); // 调用 load
return () => controller.abort(); // 组件卸载时取消请求
}, []); // 空数组
AbortController 更彻底,网络请求本身会被取消。
五、把数据渲染成列表
有了数据之后,通常要用 FlatList 渲染。这是第3课的知识,现在把它和数据获取结合起来。
// JavaScript + React Native
import { useState, useEffect } from 'react'; // 引入 useState 和 useEffect
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native'; // 引入需要的组件
export default function App() { // 定义并默认导出 App
const [posts, setPosts] = useState([]); // 文章列表,初始空数组
const [loading, setLoading] = useState(true); // 加载状态,初始 true
const [error, setError] = useState(null); // 错误状态,初始 null
useEffect(() => { // 注册副作用
let cancelled = false; // 取消标记
async function load() { // 定义异步加载函数
try { // 开始 try
const res = await fetch('https://jsonplaceholder.typicode.com/posts'); // 发请求
const json = await res.json(); // 解析 JSON
if (!cancelled) setPosts(json); // 如果没卸载,更新 posts
} catch (e) { // 捕获错误
if (!cancelled) setError(e.message); // 如果没卸载,更新 error
} finally { // 收尾
if (!cancelled) setLoading(false); // 如果没卸载,关闭 loading
} // 结束 try
} // 结束 load
load(); // 调用 load
return () => { // 返回清理函数
cancelled = true; // 卸载时设为 true
}; // 结束清理
}, []); // 空数组
if (loading) { // 加载中
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<ActivityIndicator size="large" /> // 加载指示器
</View> // 结束容器
); // 结束 return
} // 结束判断
if (error) { // 出错
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<Text>加载失败:{error}</Text> // 错误信息
</View> // 结束容器
); // 结束 return
} // 结束判断
return ( // 成功
<FlatList // 高效列表
data={posts} // 数据源
keyExtractor={(item) => item.id.toString()} // 从每项提取唯一 key
renderItem={({ item }) => ( // 如何渲染每一项
<View style={styles.item}> // 每项容器
<Text style={styles.title}>{item.title}</Text> // 显示标题
</View> // 结束容器
)} // 结束 renderItem
ItemSeparatorComponent={() => <View style={styles.separator} />} // 分隔线
/> // 结束 FlatList
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, // 居中容器样式
item: { padding: 16 }, // 列表项样式
title: { fontSize: 16, fontWeight: 'bold' }, // 标题样式
separator: { height: 1, backgroundColor: '#eee' }, // 分隔线样式
}); // 结束样式表
useEffect 负责拉数据,loading、error 负责状态展示,FlatList 负责渲染数据。整个流程依然是"视图 = 函数(状态)":状态变,视图重新计算。
六、下拉刷新
FlatList 支持下拉刷新,配合数据获取非常自然。
// JavaScript + React Native
import { useState, useEffect } from 'react'; // 引入 useState 和 useEffect
import { FlatList, Text, RefreshControl } from 'react-native'; // 引入需要的组件
export default function App() { // 定义并默认导出 App
const [posts, setPosts] = useState([]); // 文章列表,初始空数组
const [refreshing, setRefreshing] = useState(false); // 刷新状态,初始 false
const load = async () => { // 定义加载函数
const res = await fetch('https://jsonplaceholder.typicode.com/posts'); // 发请求
const json = await res.json(); // 解析 JSON
setPosts(json); // 更新 posts
}; // 结束 load
useEffect(() => { // 注册副作用
load(); // 首次加载
}, []); // 空数组
const handleRefresh = async () => { // 定义下拉刷新函数
setRefreshing(true); // 打开 refreshing
await load(); // 重新加载
setRefreshing(false); // 关闭 refreshing
}; // 结束 handleRefresh
return ( // 返回界面描述
<FlatList // 高效列表
data={posts} // 数据源
keyExtractor={(item) => item.id.toString()} // 从每项提取唯一 key
renderItem={({ item }) => <Text>{item.title}</Text>} // 渲染每一项
refreshControl={ // 下拉刷新控件
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} /> // 绑定状态和回调
} // 结束 refreshControl
/> // 结束 FlatList
); // 结束 return
} // 结束 App
RefreshControl 是 FlatList 的 refreshControl 属性值。下拉时触发 onRefresh,更新 refreshing 状态,界面自动显示刷新指示器。
七、依赖数组常见问题
7.1 依赖不完整
// JavaScript + React Native
// 错误:使用了 count,却没有写进依赖数组
useEffect(() => { // 副作用回调
console.log(count); // 使用了 count
}, []); // 依赖数组是空的
useEffect 只在挂载时执行一次,count 后续变化不会触发它。如果逻辑上依赖 count,就必须写进依赖数组。
7.2 依赖里有对象或数组
// JavaScript + React Native
useEffect(() => { // 副作用回调
// ... // 副作用内容
}, [{ id: 1 }]); // 每次渲染都创建新对象,引用不同
每次渲染都创建新对象,引用不同,useEffect 会反复执行。应该把对象拆成具体的基本类型值,比如 [id]。
7.3 无限循环
// JavaScript + React Native
// 错误:会无限循环
useEffect(() => { // 副作用回调
setCount(count + 1); // 更新 count
}, [count]); // 依赖 count
count 变化触发 useEffect,useEffect 又更新 count,无限循环。要避免在 useEffect 中更新它自己依赖的状态。
7.4 一个组件可以有多个 useEffect
一个组件里可以有多个 useEffect,每个负责一件独立的事。这是推荐做法。
// JavaScript + React Native
useEffect(() => { // 第一个副作用
loadData(); // 负责拉数据
}, []); // 空数组
useEffect(() => { // 第二个副作用
const timer = setInterval(() => {}, 1000); // 负责定时器
return () => clearInterval(timer); // 返回清理函数
}, []); // 空数组
分开写比混在一起更清晰,也更容易维护。
八、useEffect 与"视图 = 函数(状态)"
把 useEffect 放回"视图 = 函数(状态)"的框架里:
- 渲染负责"根据状态算视图";
useEffect负责"在渲染之后做副作用";- 副作用完成后,通过
setState更新状态; - 状态变化,重新渲染,视图更新。
// 概念表达
渲染 -> 副作用 -> 更新状态 -> 重新渲染 -> 新视图 // 完整闭环
副作用是"状态"的来源之一。它把外部世界的数据带进组件,再通过状态驱动界面。这就是为什么 useEffect 和"视图 = 函数(状态)"并不矛盾,而是它的补充。
九、练习题
练习题1:选择题——什么是副作用
题目:下面哪一项属于副作用?
A. 计算两个数字的和
B. 根据状态返回 JSX
C. 发起网络请求
D. 把 props 解构
参考答案:C
解读:副作用是指"计算返回值之外"的事情,比如网络请求、定时器、订阅、日志。计算、返回 JSX、解构都是纯计算,不算副作用。
练习题2:填空题——useEffect 执行时机
题目:补全下面的表格:
// 概念表达
不传依赖数组 -> 每次渲染后执行 // 不传时
传空数组 [] -> ____ // 空数组
传 [count] -> ____ // 具体依赖
参考答案:只在挂载后执行一次;挂载后执行一次,count 变化时再执行。
解读:依赖数组决定 useEffect 的执行时机。
练习题3:改错题——async 回调
题目:下面代码有一处错误,请指出并修正。
// JavaScript + React Native
useEffect(async () => { // async 回调,不允许
const res = await fetch('https://api.example.com'); // 发请求
const json = await res.json(); // 解析 JSON
setData(json); // 更新数据
}, []); // 空数组
参考答案:
// JavaScript + React Native
useEffect(() => { // 普通回调
async function load() { // 内部定义 async 函数
const res = await fetch('https://api.example.com'); // 发请求
const json = await res.json(); // 解析 JSON
setData(json); // 更新数据
} // 结束 load
load(); // 调用 load
}, []); // 空数组
解读:useEffect 的回调应该返回清理函数或 undefined。写成 async 会返回 Promise,React 无法识别。
练习题4:代码阅读题——执行顺序
题目:阅读代码,说出控制台打印顺序。
// JavaScript + React Native
function App() { // 定义组件
console.log('渲染'); // 渲染时打印
useEffect(() => { // 注册副作用
console.log('副作用'); // 副作用执行时打印
}, []); // 空数组
return <Text>你好</Text>; // 返回界面描述
} // 结束组件
参考答案:先打印"渲染",后打印"副作用"。
解读:useEffect 在渲染完成之后执行。
练习题5:动手题——挂载时打印
题目:写一个 useEffect,让组件挂载时在控制台打印"组件已挂载"。
参考答案:
// JavaScript + React Native
useEffect(() => { // 副作用回调
console.log('组件已挂载'); // 打印日志
}, []); // 空数组
解读:空数组表示只在挂载后执行一次。
练习题6:简答题——三种依赖数组的区别
题目:简述 useEffect 不传依赖数组、传空数组、传具体依赖三种情况的区别。
参考答案:不传时每次渲染后都执行;传空数组时只在挂载后执行一次;传具体依赖时挂载后执行一次,依赖变化时再执行。
解读:依赖数组是控制副作用执行时机的开关。
练习题7:清理题——定时器
题目:写一个 useEffect,启动一个每秒打印一次的定时器,并在组件卸载时清除。
参考答案:
// JavaScript + React Native
useEffect(() => { // 副作用回调
const timer = setInterval(() => { // 启动定时器
console.log('每秒一次'); // 每秒打印
}, 1000); // 间隔 1000 毫秒
return () => { // 返回清理函数
clearInterval(timer); // 清除定时器
}; // 结束清理函数
}, []); // 空数组
解读:定时器必须在组件卸载时清除,否则会持续运行,造成内存泄漏。
练习题8:数据获取题——fetch
题目:写一个 useEffect,组件挂载时请求 https://jsonplaceholder.typicode.com/posts/1,并把结果保存到 data 状态。
参考答案:
// JavaScript + React Native
useEffect(() => { // 副作用回调
async function load() { // 定义 async 函数
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 发请求
const json = await res.json(); // 解析 JSON
setData(json); // 更新 data
} // 结束 load
load(); // 调用 load
}, []); // 空数组
解读:在 useEffect 内部定义 async 函数并调用。空数组保证只执行一次。
练习题9:加载状态题
题目:在数据获取中加入 loading 状态,加载中显示"加载中…"。
参考答案:
// JavaScript + React Native
const [loading, setLoading] = useState(true); // 创建状态 loading,初始 true
useEffect(() => { // 副作用回调
async function load() { // 定义 async 函数
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 发请求
const json = await res.json(); // 解析 JSON
setData(json); // 更新 data
setLoading(false); // 关闭 loading
} // 结束 load
load(); // 调用 load
}, []); // 空数组
if (loading) return <Text>加载中...</Text>; // 加载中显示提示
解读:loading 初始为 true,请求完成后设为 false。界面根据 loading 显示不同内容。
练习题10:错误处理题
题目:在数据获取中加入 error 状态,出错时显示错误信息。
参考答案:
// JavaScript + React Native
const [error, setError] = useState(null); // 创建状态 error,初始 null
useEffect(() => { // 副作用回调
async function load() { // 定义 async 函数
try { // 开始 try
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 发请求
if (!res.ok) throw new Error(`请求失败:${res.status}`); // 检查状态码
const json = await res.json(); // 解析 JSON
setData(json); // 更新 data
} catch (e) { // 捕获错误
setError(e.message); // 更新 error
} // 结束 try
} // 结束 load
load(); // 调用 load
}, []); // 空数组
if (error) return <Text>出错:{error}</Text>; // 出错时显示错误信息
解读:用 try/catch 捕获错误,用 error 状态保存错误信息。
练习题11:依赖项题——缺少依赖
题目:下面代码想监听 count 变化,但依赖数组不完整。请修正。
// JavaScript + React Native
useEffect(() => { // 副作用回调
console.log(count); // 使用了 count
}, []); // 依赖数组是空的
参考答案:
// JavaScript + React Native
useEffect(() => { // 副作用回调
console.log(count); // 使用了 count
}, [count]); // 依赖 count
解读:useEffect 里使用了 count,就必须把 count 写进依赖数组。
练习题12:代码阅读题——无限循环
题目:下面代码为什么会导致无限循环?如何修正?
// JavaScript + React Native
useEffect(() => { // 副作用回调
setCount(count + 1); // 更新 count
}, [count]); // 依赖 count
参考答案:count 变化触发 useEffect,useEffect 又更新 count,形成循环。如果只是想初始化一次,应该用 [],或者避免在 useEffect 中更新它自己依赖的状态。
解读:依赖和它更新的状态存在循环依赖,就会无限循环。
练习题13:取消请求题
题目:写一个 useEffect,请求数据,并在组件卸载时取消请求(使用 cancelled 标记)。
参考答案:
// JavaScript + React Native
useEffect(() => { // 副作用回调
let cancelled = false; // 定义取消标记
async function load() { // 定义 async 函数
try { // 开始 try
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 发请求
const json = await res.json(); // 解析 JSON
if (!cancelled) setData(json); // 如果没卸载,更新 data
} catch (e) { // 捕获错误
if (!cancelled) setError(e.message); // 如果没卸载,更新 error
} // 结束 try
} // 结束 load
load(); // 调用 load
return () => { // 返回清理函数
cancelled = true; // 组件卸载时设为 true
}; // 结束清理
}, []); // 空数组
解读:cancelled 是 useEffect 内部的局部变量。组件卸载时,清理函数把它设为 true,请求返回后判断 cancelled,避免在已卸载组件上更新状态。
练习题14:综合题——用户列表
题目:用 useEffect 请求 https://jsonplaceholder.typicode.com/users,用 FlatList 渲染用户列表,加载中显示加载指示器,出错显示错误信息。
参考答案:
// JavaScript + React Native
import { useState, useEffect } from 'react'; // 引入 useState 和 useEffect
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native'; // 引入需要的组件
export default function App() { // 定义并默认导出 App
const [users, setUsers] = useState([]); // 用户列表,初始空数组
const [loading, setLoading] = useState(true); // 加载状态,初始 true
const [error, setError] = useState(null); // 错误状态,初始 null
useEffect(() => { // 注册副作用
let cancelled = false; // 取消标记
async function load() { // 定义 async 函数
try { // 开始 try
const res = await fetch('https://jsonplaceholder.typicode.com/users'); // 发请求
const json = await res.json(); // 解析 JSON
if (!cancelled) setUsers(json); // 如果没卸载,更新 users
} catch (e) { // 捕获错误
if (!cancelled) setError(e.message); // 如果没卸载,更新 error
} finally { // 收尾
if (!cancelled) setLoading(false); // 如果没卸载,关闭 loading
} // 结束 try
} // 结束 load
load(); // 调用 load
return () => { // 返回清理函数
cancelled = true; // 卸载时设为 true
}; // 结束清理
}, []); // 空数组
if (loading) { // 加载中
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<ActivityIndicator size="large" /> // 加载指示器
</View> // 结束容器
); // 结束 return
} // 结束判断
if (error) { // 出错
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<Text>加载失败:{error}</Text> // 错误信息
</View> // 结束容器
); // 结束 return
} // 结束判断
return ( // 成功
<FlatList // 高效列表
data={users} // 数据源
keyExtractor={(item) => item.id.toString()} // 从每项提取唯一 key
renderItem={({ item }) => ( // 如何渲染每一项
<View style={styles.item}> // 每项容器
<Text style={styles.name}>{item.name}</Text> // 显示姓名
<Text style={styles.email}>{item.email}</Text> // 显示邮箱
</View> // 结束容器
)} // 结束 renderItem
ItemSeparatorComponent={() => <View style={styles.separator} />} // 分隔线
/> // 结束 FlatList
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, // 居中容器样式
item: { padding: 16 }, // 列表项样式
name: { fontSize: 16, fontWeight: 'bold' }, // 姓名样式
email: { fontSize: 14, color: '#666' }, // 邮箱样式
separator: { height: 1, backgroundColor: '#eee' }, // 分隔线样式
}); // 结束样式表
解读:useEffect 负责数据获取,三态负责界面切换,FlatList 负责渲染列表。cancelled 防止在已卸载组件上更新状态。
练习题15:综合应用题——带刷新和重试的文章列表
题目:创建一个文章列表应用:
- 加载时显示"加载中…";
- 成功时用
FlatList显示文章标题; - 失败时显示错误信息和"重试"按钮;
- 支持下拉刷新。
参考答案:
// JavaScript + React Native
import { useState, useEffect } from 'react'; // 引入 useState 和 useEffect
import { View, Text, FlatList, ActivityIndicator, Button, RefreshControl, StyleSheet } from 'react-native'; // 引入需要的组件
export default function App() { // 定义并默认导出 App
const [posts, setPosts] = useState([]); // 文章列表,初始空数组
const [loading, setLoading] = useState(true); // 加载状态,初始 true
const [error, setError] = useState(null); // 错误状态,初始 null
const [refreshing, setRefreshing] = useState(false); // 刷新状态,初始 false
const load = async () => { // 定义加载函数
try { // 开始 try
setError(null); // 清空 error
const res = await fetch('https://jsonplaceholder.typicode.com/posts'); // 发请求
if (!res.ok) throw new Error(`请求失败:${res.status}`); // 检查状态码
const json = await res.json(); // 解析 JSON
setPosts(json); // 更新 posts
} catch (e) { // 捕获错误
setError(e.message); // 更新 error
} finally { // 收尾
setLoading(false); // 关闭 loading
setRefreshing(false); // 关闭 refreshing
} // 结束 try
}; // 结束 load
useEffect(() => { // 注册副作用
load(); // 首次加载
}, []); // 空数组
const handleRefresh = () => { // 定义下拉刷新函数
setRefreshing(true); // 打开 refreshing
load(); // 重新加载
}; // 结束 handleRefresh
if (loading) { // 加载中
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<ActivityIndicator size="large" /> // 加载指示器
<Text>加载中...</Text> // 加载提示
</View> // 结束容器
); // 结束 return
} // 结束判断
if (error) { // 出错
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<Text>出错了:{error}</Text> // 错误信息
<Button // 重试按钮
title="重试" // 按钮文字
onPress={() => { // 点击回调
setLoading(true); // 打开 loading
load(); // 重新加载
}} // 结束回调
/> // 结束 Button
</View> // 结束容器
); // 结束 return
} // 结束判断
return ( // 成功
<FlatList // 高效列表
data={posts} // 数据源
keyExtractor={(item) => item.id.toString()} // 从每项提取唯一 key
renderItem={({ item }) => ( // 如何渲染每一项
<View style={styles.item}> // 每项容器
<Text style={styles.title}>{item.title}</Text> // 显示标题
</View> // 结束容器
)} // 结束 renderItem
ItemSeparatorComponent={() => <View style={styles.separator} />} // 分隔线
refreshControl={ // 下拉刷新控件
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} /> // 绑定状态和回调
} // 结束 refreshControl
/> // 结束 FlatList
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, // 居中容器样式
item: { padding: 16 }, // 列表项样式
title: { fontSize: 16, fontWeight: 'bold' }, // 标题样式
separator: { height: 1, backgroundColor: '#eee' }, // 分隔线样式
}); // 结束样式表
解读:这是数据获取的完整形态。load 负责请求和状态更新;loading、error、posts、refreshing 四个状态分别管理不同场景;FlatList 负责渲染;RefreshControl 负责下拉刷新。所有状态变化都会触发重新渲染,界面自动切换。
十、总结
这一课我们学习了如何在 React Native 中处理副作用和数据获取。
核心要点:
- 组件渲染应该是纯的,只负责根据状态计算视图;
- 副作用放在
useEffect中执行; useEffect在渲染完成后执行;- 依赖数组决定执行时机:不传、空数组、具体依赖;
- 清理函数用于善后,避免内存泄漏;
- 数据获取通常用
data、loading、error三态管理; useEffect回调不能直接写成async,要在内部定义async函数;- 用
cancelled标记或AbortController避免在已卸载组件上更新状态; - 获取到的数据可以用
FlatList渲染; - 下拉刷新用
RefreshControl; - 一个组件里可以有多个
useEffect,每个负责一件独立的事。
记住一句话:
渲染负责算视图,副作用负责带数据;数据通过状态流回,状态驱动视图更新。
第5课预告
下一课,我们进入 【React Native娓娓道来】第5课 导航与多页面:用 React Navigation 组织页面流。
更多推荐



所有评论(0)