React 第4周笔记useEffect +异步数据获取
路线图阶段:第一阶段(React 基础)第4周 项目:小相册(从 Picsum API 拉图片) 核心:学会”在组件里安全地做副作用”——尤其是从 API 取数据 心智模型延续:
UI = f(state),但有些事(发请求、操作 DOM)不属于”算 UI”,要单独处理
1. 为什么需要 useEffect:副作用(Side Effect)
组件函数的本职工作是”根据 state 算出 UI 并返回”,这个过程应该是纯粹的。 但有些操作和”算 UI”无关、又必须做:
- 向 API 请求数据
- 手动操作 DOM
- 设置定时器(setInterval / setTimeout)
这些叫副作用(side effect)。它们不能直接写在组件函数体里。
反面教材:直接在组件体里 fetch → 无限循环
function Gallery() {
const [photos, setPhotos] = useState([]);
fetch("...").then(r => r.json()).then(data => setPhotos(data)); // ❌
return <div>{photos.length} 张</div>;
}
为什么死循环:
渲染 → 执行 fetch → setPhotos → 触发重渲染 → 又执行 fetch → 又 setPhotos → ∞
根本原因:副作用(fetch)不该和”渲染”绑在一起,每次渲染都跑就出事。
2. useEffect 基本结构
import { useState, useEffect } from "react";
useEffect(() => {
// 副作用写在这里
}, []); // ← 依赖数组(dependency array)
useEffect 把副作用从”渲染过程”里隔离出来,让你控制它什么时候跑。
3. 依赖数组(Dependency Array):副作用的”监视清单”
依赖数组告诉 React:只有清单里的值变了,才重新执行 effect。
useEffect(() => { ... }, []);
// 空清单 → 没有要监视的东西 → 只在挂载时跑一次(最常用于初次取数据)
useEffect(() => { ... }, [query]);
// 监视 query → 每次 query 变了才重新跑(如搜索关键词变了重新请求)
useEffect(() => { ... });
// 不写依赖数组 → 每次渲染都跑 → 这就是上面那个死循环的来源
推论:
[query]的 effect,如果 query 没变,即使组件因为别的 state 重渲染了很多次,也不会执行。 这正是依赖数组的价值:精确控制副作用何时跑,而不是每次渲染都跑。
类比设计:依赖数组像 Figma 的”响应式约束”——设定”当父容器宽度变化时才重新布局”, 依赖数组就是那个”当 X 变化时才执行”的条件。
4. 异步基础:Promise / async / await
(这部分是 JS 基础,不是 React;路线图第7-10周会系统学,这里够用即可)
fetch 是异步的
网络请求发出去不会立刻有结果,要等服务器回应。
fetch 不直接返回数据,而是返回一个 Promise(承诺)——“将来会给你结果”的占位符。
类比:便利店点现做便当,先给你一个”取餐号”(Promise),做好了再叫你。
Promise 的两层(易混点)
async function loadPhotos() {
const res = await fetch("..."); // 第一层:fetch 的 Promise,被 await 拆开
const data = await res.json(); // 第一层:res.json() 的 Promise,被 await 拆开
}
const result = loadPhotos(); // 第二层:async 函数整体,调用时又返回一个 Promise
- 内部的 Promise 被
await消化掉了。 - 但只要函数标了
async,它整体被调用时一定再返回一个 Promise(语法固有行为)。 - 这两层是不同的 Promise。
.then() 链 vs async/await(等价)
// .then() 写法
fetch("...")
.then(res => res.json()) // res 是"信封",res.json() 拆开成数据(也是异步,所以要第二个 then)
.then(data => setPhotos(data));
// async/await 写法(更直观,读起来像同步)
async function loadPhotos() {
const res = await fetch("...");
const data = await res.json();
setPhotos(data);
}
await= “等这个 Promise 兑现,拿到结果再往下走”。- ⚠️
await只能写在标了async的函数里。 res.json()本身也是异步、也返回 Promise,所以也要 await(或第二个 then)。
一条链的流程
发请求 → 拿到信封(res) → 拆信封变数据(data) → 用数据
5. useEffect 里调用 async 函数的正确姿势
❌ 不能直接把 async 函数传给 useEffect
useEffect(loadPhotos, []); // ❌ async 函数返回 Promise,违反 useEffect 约定,报警告
useEffect 的第一个参数要么不返回东西,要么返回一个”清理函数”。
async 函数总返回 Promise → 违反约定。
✅ 在 useEffect 内部定义并调用(主流写法)
useEffect(() => {
async function loadPhotos() {
const res = await fetch("...");
const data = await res.json();
setPhotos(data);
}
loadPhotos(); // 定义完立刻调用
}, []);
外层是普通箭头函数(返回 undefined,不返回 Promise),内部跑 async 逻辑。
6. 三状态:loading / error / data
真实 API 请求有三种状态,都要呈现给用户(只给空白页体验差)。
const [photos, setPhotos] = useState([]);
const [loading, setLoading] = useState(true); // 初始 true:组件一挂载就要请求
const [error, setError] = useState(null);
loading 的判断逻辑(一句话)
开始等之前 = true,结果到手(成功或失败)= false。
挂载 → loading=true(显示"加载中")
→ 发请求(等待中,仍 true)
→ 结果回来:成功 setLoading(false) / 失败也 setLoading(false)
→ loading=false(显示照片或错误)
⚠️ 成功和失败都要 setLoading(false)。 只在成功时设,一旦请求失败,loading 永远卡在 true → 用户永远看到”加载中” → 高频 bug。
⚠️ 结束加载用
setLoading(false),不要用setLoading(!loading)。!loading是”翻转”,语义是”来回切”(用于主题切换那种);这里意图是”明确设成 false”。 即使!true碰巧等于 false,也要写准确意图。
7. 渲染列表的两个坑(复习)
map 用 {} 必须 return
// ❌ 用 {} 却没 return → 每项返回 undefined → 一张图都不显示
{photos.map((photo) => {
<div key={photo.id}>...</div>;
})}
// ✅ 用 () 自动返回
{photos.map((photo) => (
<div key={photo.id}>...</div>
))}
// ✅ 或用 {} + 手动 return
{photos.map((photo) => {
return <div key={photo.id}>...</div>;
})}
error 是对象,不能直接渲染
catch (error) {
setError(error.message); // ✅ 存字符串(.message),不要存整个 Error 对象
}
// JSX 里直接 {error}(对象)会报错
8. Picsum API 实用要点
- 不需要 API key,不用注册,适合练手(限速约 1000 次/分钟,足够)。
- 取图片列表:
https://picsum.photos/v2/list?page=1&limit=10- 返回一个数组,每项含
id/author/width/height/download_url等。 - ⚠️
download_url是原图,非常大(如 5616×3744),直接用会卡。
- 返回一个数组,每项含
- 技巧:用 id 自己拼缩略图 URL(指定尺寸):
<img src={`https://picsum.photos/id/${photo.id}/300/200`} alt={photo.author} /> // 模板字符串:反引号包裹,${} 插变量,比字符串拼接干净
完整代码:小相册 Gallery
import { useState, useEffect } from "react";
function Gallery() {
const [photos, setPhotos] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadPhotos() {
try {
const res = await fetch("https://picsum.photos/v2/list?page=1&limit=10");
const data = await res.json();
setPhotos(data);
setLoading(false);
} catch (error) {
setError(error.message);
setLoading(false);
}
}
loadPhotos();
}, []);
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了:{error}</p>;
return (
<div style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "12px",
}}>
{photos.map((photo) => (
<div key={photo.id}>
<img
src={`https://picsum.photos/id/${photo.id}/300/200`}
alt={photo.author}
style={{ width: "100%", display: "block", borderRadius: "8px" }}
/>
</div>
))}
</div>
);
}
function App() {
return <Gallery />;
}
export default App;
第4周验收
- 能解释依赖数组
[]的作用(只挂载时跑一次) - 理解为什么不能直接在组件体里 fetch(死循环)
- 会用 async/await + try/catch 取数据
- 会处理 loading / error / data 三状态
- 项目:小相册(API 取图 + Grid 布局)