数据请求基础
本教程共 50 篇 · 第 35 篇 · 更新于 2026-07-29 · 约 8 分钟阅读
35. 数据请求基础
本节目标:学会在组件里正确地发数据请求,管好加载态和错误态,避开竞态条件这个坑。
为什么要在 Effect 里请求数据
React 组件的渲染必须是纯函数——只负责算出 JSX,不能干”请求网络”这种事。
所以数据请求要放到副作用(effect)里。useEffect 在浏览器绘制完之后执行,正好适合做网络请求。
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setIsLoading(true);
setError(null);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('请求失败');
return res.json();
})
.then(data => setUser(data))
.catch(err => setError(err.message))
.finally(() => setIsLoading(false));
}, [userId]);
if (isLoading) return <p>加载中……</p>;
if (error) return <p>出错了:{error}</p>;
return <h1>{user.name}</h1>;
}
这个模式你应该记住:三个 state —— 数据、加载态、错误态。Effect 里根据 userId 的变化重新请求。
Note依赖数组里放
userId,意味着切换用户时会重新发请求。不放依赖数组就是只请求一次。
async/await 的写法
上面的 Promise 链可以改成 async/await,更好读:
useEffect(() => {
async function loadUser() {
setIsLoading(true);
setError(null);
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('请求失败');
const data = await res.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
}
loadUser();
}, [userId]);
注意:useEffect 的回调函数不能直接写成 async,因为 async 函数会返回 Promise,而 React 要么期望你返回 undefined,要么返回清理函数。所以要在 Effect 内部再包一个 async 函数来调用。
竞态条件:最容易踩的坑
想象一下这个场景:你在筛选列表,快速切换选项。每次切换都发一个请求。问题是——后发的请求可能比先发的先回来。
先点 Alice,再点 Bob,再点 Taylor。如果 Taylor 的响应比 Bob 先到,页面会短暂显示 Taylor,然后又被 Bob 的响应覆盖。这就是竞态条件。
// 有 bug 的版本
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
修复办法是用清理函数标记”过期”的请求:
useEffect(() => {
let cancelled = false;
async function loadUser() {
setIsLoading(true);
try {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) setUser(data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setIsLoading(false);
}
}
loadUser();
return () => {
cancelled = true;
};
}, [userId]);
每次 Effect 重新执行,清理函数会把上一次标记为 true。这样过时的响应即使回来也不会更新 state。
Warning我见过很多人在 Effect 里直接写
fetch但忘了处理竞态条件。测试环境网速快可能测不出来,生产环境就露馅了。
用 AbortController 取消请求
ignore 标记只能阻止 setState,请求本身还在跑。想真正取消请求,用 AbortController:
useEffect(() => {
const controller = new AbortController();
async function loadUser() {
try {
const res = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
const data = await res.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
}
}
loadUser();
return () => controller.abort();
}, [userId]);
controller.abort() 会中止正在进行的 fetch。fetch 被中断后会抛出 AbortError,我们判断一下忽略掉就行。
抽成自定义 Hook
数据请求的逻辑到处重复,不如抽成一个自定义 Hook:
function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const controller = new AbortController();
async function load() {
setIsLoading(true);
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!cancelled) setData(json);
} catch (err) {
if (!cancelled && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
if (!cancelled) setIsLoading(false);
}
}
load();
return () => {
cancelled = true;
controller.abort();
};
}, [url]);
return { data, isLoading, error };
}
用起来就很干净了:
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useFetch(`/api/users/${userId}`);
if (isLoading) return <p>加载中……</p>;
if (error) return <p>出错了:{error}</p>;
return <h1>{user.name}</h1>;
}
什么时候不该用 Effect 请求数据
在 Effect 里写 fetch 是最基础的方式,但也有不少弊端:
- 服务端渲染(SSR)时 Effect 不执行,HTML 里只有 loading
- 父子组件各自请求容易形成”网络瀑布”
- 没有缓存,组件重新挂载就得重新请求
React 19 推荐用 use Hook 或框架内置的数据获取方案(比如 React Router 的 loader、Next.js 的服务端组件)。这些内容后面章节会讲。
Tip如果你只是写个简单的客户端应用,用 Effect 请求就够了。一旦涉及 SSR 或复杂数据依赖,考虑上 TanStack Query 或 SWR 这类库。