
如果你看过《2026年前端的痛点:90%开发者还在错误地处理数据获取》,那么你已经理解了网络模型的基础。但真正的挑战才刚开始。 这一篇,我们要解决一个80%开发者都被绊倒的问题:async/await看起来简洁,但它隐藏的陷阱比Promise链条多得多。
我最近在一个团队的Code Review中,看到了这样的代码:
async function processPayment(orderId, amount) {
// 创建订单
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({ orderId, amount })
});
// 发送支付确认邮件
await fetch('/api/emails/send', {
method: 'POST',
body: JSON.stringify({ to: userEmail })
});
}
表面上看没问题。但在生产环境的一个高峰期,这个函数的响应时间从原来的500ms暴增到2秒。为什么?因为两个请求是串行的,邮件服务器的响应时间增加了1.5秒,导致整个支付流程卡顿。
更严重的是,如果邮件服务宕机了怎么办?整个支付流程就卡死了,用户必须重新发起支付。
这是一个常见的async/await陷阱:代码看起来简洁,但隐藏着串行执行的性能问题和错误处理的缺陷。
你的代码会不会也在默默地拖累你的应用性能?
很多初级开发者这样理解async/await:
"async/await就是Promise的语法糖,把
.then()改成await就行了"
这个理解害了很多人。
async/await不仅仅是语法转换,它改变了代码的执行逻辑。最关键的区别:
// Promise链:请求是并发发送的吗?
Promise.all([
fetch('/api/user'),
fetch('/api/posts')
])
// async/await:这是什么情况?
asyncfunction getData() {
const user = await fetch('/api/user'); // ← 等这个完成
const posts = await fetch('/api/posts'); // ← 再发这个
return { user, posts };
}
看上去一样吗?完全不一样。
对比维度 | Promise.all | async/await(顺序) |
|---|---|---|
网络行为 | 两个请求同时发送 | 一个接一个 |
总耗时 | max(300ms, 300ms) = 300ms | 300ms + 300ms = 600ms |
服务器压力 | ↓ | ↑ |
用户感受 | 快速 | 慢速 |
这就是为什么有人的API调用性能比别人差两倍——不是因为浏览器慢,而是因为他们的async/await代码是串行的。
要理解async/await,需要从底层看:
// 第一步:async函数总是返回Promise
asyncfunction fetchUser() {
return'user data'; // 不是返回字符串,而是返回 Promise<'user data'>
}
// 所以这样用:
const promise = fetchUser(); // 这是一个Promise
promise.then(user =>console.log(user));
// 第二步:await暂停执行,等待Promise解决
asyncfunction example() {
console.log('开始');
const result = await somePromise(); // ← 代码在这里暂停,直到Promise resolve
console.log('结束'); // ← 只有resolve之后才能到这里
}
换个比喻理解:
同步代码(阻塞):
你在银行排队,从第1个人开始等,每个人办完才轮到下一个
总时间 = 人数 × 每人用时
Promise链(非阻塞):
你在多个银行的ATM机前同时排队,取款时间重叠
总时间 = max(所有队列的时间)
async/await(假如写成顺序):
你同样排多个队列,但必须一个接一个地等
总时间 = 所有队列的时间相加 ❌ 这是浪费时间
看看这段代码(出自很多教程):
// "简洁的async/await版本"
asyncfunction getUser() {
const userResponse = await fetch('/api/user');
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
}
教程说这"更可读"。但问题是:这是串行请求,性能很差。
更好的写法(大多教程都没教):
// ✅ 应该这样写
asyncfunction getUser() {
// 并发请求两个API,但我们需要user.id,所以必须先等user
const userResponse = await fetch('/api/user');
const user = await userResponse.json();
// 这里再开始posts请求是合理的,因为需要user.id
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
}
// ✅✅ 真正高效的写法:当两个请求独立时
asyncfunction getUserAndPosts() {
// 同时发送两个请求,再等待结果
const [userResponse, otherDataResponse] = awaitPromise.all([
fetch('/api/user'),
fetch('/api/other-data') // 这个不依赖user.id
]);
const user = await userResponse.json();
const otherData = await otherDataResponse.json();
return { user, otherData };
}
2026年的现实:有经验的工程师会用async/await结合Promise.all()。初级开发者则盲目追求"简洁",写出串行的垃圾代码。
// ❌ 初级开发者的理解:两者是一样的
const p1 = fetch(url).catch(err => console.error(err));
const p2 = (async () => {
try {
await fetch(url);
} catch (err) {
console.error(err);
}
})();
// 看上去一样?不对!
区别在于错误的"范围":
// ❌ 错误做法:不会捕获JSON解析错误
asyncfunction fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) thrownewError('HTTP error');
} catch (error) {
console.error(error); // 只捕获fetch和response.ok的错误
}
// 这行没在try block里!
const data = await response.json(); // 如果JSON格式错误,会抛出未捕获的错误
return data;
}
// ✅ 正确做法:所有可能的错误都在try block里
asyncfunction fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
thrownewError(`HTTP ${response.status}`);
}
const data = await response.json(); // 这也在try block里
// 数据验证也应该在try block里
if (!data.id) {
thrownewError('数据格式错误');
}
return data;
} catch (error) {
console.error('数据获取失败:', error);
returnnull;
}
}
// ❌ 这样用会出问题
async function getUser() {
return user; // 你以为返回的是user对象
}
// 实际上getUser()返回的是 Promise<user>
const user = getUser(); // 这是Promise,不是user对象!
console.log(user.name); // undefined,因为Promise没有name属性
// ✅ 必须这样用
const user = await getUser();
console.log(user.name); // 现在才是真正的user对象
这个问题在回调函数中特别容易出现:
// ❌ 常见错误:在map中用async函数
const users = [1, 2, 3];
const userData = users.map(async (id) => {
returnawait fetch(`/api/users/${id}`).then(r => r.json());
});
console.log(userData); // [Promise, Promise, Promise] ❌
// userData[0].name 是 undefined,因为userData[0]是Promise
// ✅ 正确做法
const users = [1, 2, 3];
const userData = awaitPromise.all(
users.map(id => fetch(`/api/users/${id}`).then(r => r.json()))
);
console.log(userData); // [user1, user2, user3] ✅
// ❌ 遗漏await的真实案例
asyncfunction updateUserAndFetchPosts(userId, newData) {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(newData)
});
// 忘记 await response.json()
const updated = response.json(); // ← 这返回Promise,不是数据!
// 下面的代码可能在JSON还没解析完就执行
const posts = await fetch(`/api/posts?userId=${updated.id}`); // ← updated是Promise,不是对象
return posts.json();
}
// ✅ 正确做法
asyncfunction updateUserAndFetchPosts(userId, newData) {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(newData)
});
const updated = await response.json(); // ← await!
const posts = await fetch(`/api/posts?userId=${updated.id}`);
return posts.json();
}
你有没有遇到过这样的错误?
错误:CORS policy: No 'Access-Control-Allow-Origin' header
或者:
错误:401 Unauthorized
这些都是Header的问题。Headers不仅仅是"配置项",**它们定义了你和服务器之间的"握手协议"**。
fetch('https://api.bytedance.com/v1/users', {
headers: {
'Content-Type': 'application/json', // ① 我发送什么格式
'Accept': 'application/json', // ② 我期望接收什么格式
'Authorization': 'Bearer your-jwt-token', // ③ 我的身份验证
'X-Request-ID': 'uuid-12345', // ④ 用于追踪和日志
'User-Agent': 'MyApp/1.0.0', // ⑤ 我是谁
}
});
让我逐个深入:
// ❌ 常见错误:忘记设置Content-Type
asyncfunction createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData) // 发送JSON
// 忘记了headers!
});
}
// 服务器会怎样?有些服务器会拒绝,因为它不知道你发的是什么格式
// 特别是严格的企业API
// ✅ 正确做法
asyncfunction createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'// 告诉服务器:我发的是JSON
},
body: JSON.stringify(userData)
});
if (!response.ok) thrownewError(`HTTP ${response.status}`);
return response.json();
}
不同的Content-Type用在不同场景:
// JSON API(现代应用最常用)
headers: { 'Content-Type': 'application/json' }
body: JSON.stringify({ name: 'Alice' })
// 表单提交(传统网站)
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
body: new URLSearchParams({ name: 'Alice' })
// 文件上传
const formData = new FormData();
formData.append('file', fileInput.files[0]);
headers: {
// ❌ 不要设置Content-Type!浏览器会自动设置multipart/form-data
}
body: formData
// 纯文本
headers: { 'Content-Type': 'text/plain' }
body: 'Hello World'
这是最容易出错的地方:
// ❌ 常见错误:硬编码token
const TOKEN = 'eyJhbGciOiJIUzI1NiIs...'; // 千万别这样!
asyncfunction fetchUserData() {
const response = await fetch('/api/me', {
headers: {
'Authorization': `Bearer ${TOKEN}`
}
});
}
// 问题:
// 1. Token暴露在源代码里
// 2. Token失效后无法更新
// 3. 无法处理刷新token的逻辑
正确的做法(企业级应用的标准实践):
// 从localStorage或sessionStorage读取(仅用于演示,生产环境有更多考虑)
function getAuthToken() {
return localStorage.getItem('auth_token');
}
// 创建一个"认证的fetch"包装
asyncfunction authenticatedFetch(url, options = {}) {
const token = getAuthToken();
if (!token) {
// 没有token,跳转到登录页
window.location.href = '/login';
return;
}
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
// 只在需要时添加Authorization header
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
try {
const response = await fetch(url, {
...options,
headers
});
// 如果返回401,说明token过期,需要刷新
if (response.status === 401) {
// 尝试用refresh token重新获取access token
const newToken = await refreshToken();
if (!newToken) {
// 刷新失败,跳转到登录
window.location.href = '/login';
returnnull;
}
// 用新token重试原始请求
return authenticatedFetch(url, options);
}
return response;
} catch (error) {
console.error('认证请求失败:', error);
throw error;
}
}
// 现在可以这样使用
const userData = await authenticatedFetch('/api/me').then(r => r.json());
// 大型应用常见的做法:添加请求追踪ID
function makeRequest(url, options = {}) {
const requestId = generateUUID();
return fetch(url, {
...options,
headers: {
'X-Request-ID': requestId, // 用于后端日志关联
'X-Timestamp': newDate().toISOString(),
'X-Client-Version': '2.1.0', // 用于服务端兼容性检查
...options.headers
}
});
}
// 后端收到这些headers后,可以:
// 1. 记录请求的完整链路(分布式追踪)
// 2. 识别是哪个版本的客户端出现了问题
// 3. 计算延迟、统计等
const response = await fetch('/api/data');
// 响应的headers告诉你很多信息
console.log(response.headers.get('Content-Type')); // 返回数据格式
console.log(response.headers.get('Cache-Control')); // 缓存策略
console.log(response.headers.get('X-RateLimit-Remaining')); // 剩余请求配额
console.log(response.headers.get('ETag')); // 用于缓存验证
console.log(response.headers.get('Set-Cookie')); // 服务器设置的cookies
这些headers为什么重要?
// ❌ 没有理解Cache-Control的结果
asyncfunction getUser(userId) {
// 每次都发送请求,即使数据没变
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// ✅ 理解Cache-Control的做法
asyncfunction getUser(userId) {
const response = await fetch(`/api/users/${userId}`);
// 检查缓存策略
const cacheControl = response.headers.get('cache-control');
const maxAge = cacheControl?.match(/max-age=(\d+)/)?.[1];
if (maxAge) {
// 服务器说"这个数据可以缓存N秒"
// 我们可以在缓存中保存它,不用每次都请求
await cacheData(`user_${userId}`, response.json(), parseInt(maxAge));
}
return response.json();
}
// ❌ 错误的选择:用POST做列表查询
asyncfunction searchUsers(filters) {
// 不要这样做!
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(filters)
});
}
// ✅ 正确的选择
asyncfunction searchUsers(filters) {
// GET用于查询,更符合HTTP语义
const params = new URLSearchParams(filters);
const response = await fetch(`/api/users?${params}`, {
method: 'GET'
});
}
为什么这个区别这么重要?
用POST查询列表:
├─ ❌ 浏览器不会缓存
├─ ❌ CDN无法加速
├─ ❌ 网络代理无法识别
├─ ❌ SEO差(爬虫不会跟随POST)
└─ ❌ 服务器成本提高30-50%
用GET查询列表:
├─ ✅ 浏览器自动缓存
├─ ✅ CDN加速
├─ ✅ 网络代理支持
├─ ✅ SEO友好
└─ ✅ 服务器成本更低
这就是为什么设计良好的API永远用GET查询,用POST创建。
// ❌ 手动拼接URL(容易出错)
const query = `page=${page}&limit=${limit}&sort=${sort}`;
const url = `/api/users?${query}`;
// ❌ 如果参数包含特殊字符会出问题
const name = "Smith & Sons"; // & 会被误解
const url = `/api/users?name=${name}`; // 错误的URL
// ✅ 使用URLSearchParams(自动处理编码)
const params = new URLSearchParams({
page: 1,
limit: 10,
sort: 'name',
name: "Smith & Sons"// 自动进行URL编码
});
const url = `/api/users?${params}`; // /api/users?page=1&limit=10&sort=name&name=Smith%20%26%20Sons
// 场景1:创建新用户(POST)
asyncfunction createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
// POST 成功通常返回201 Created
if (response.status === 201) {
const newUser = await response.json();
console.log('创建成功,新用户ID:', newUser.id);
return newUser;
}
}
// 场景2:完整替换用户数据(PUT)
asyncfunction replaceUser(userId, completeUserData) {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: completeUserData.name,
email: completeUserData.email,
age: completeUserData.age,
avatar: completeUserData.avatar,
// 必须包含所有字段
// 如果缺少某个字段,服务器会把它设为null或删除
})
});
return response.json();
}
// 场景3:只改某个字段(PATCH)
asyncfunction updateUserEmail(userId, newEmail) {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: newEmail }) // 只改这一个
});
return response.json();
}
// 场景4:删除用户(DELETE)
asyncfunction deleteUser(userId) {
const response = await fetch(`/api/users/${userId}`, {
method: 'DELETE'
});
// DELETE通常返回204 No Content(无响应体)
if (response.status === 204) {
console.log('删除成功');
return { success: true };
}
// 有些API返回200和一个确认信息
const result = await response.json();
return result;
}
PUT vs PATCH 的真实区别:
假设服务器有这样的用户数据:
{
id: 1,
name: 'Alice',
email: 'alice@example.com',
age: 25,
avatar: 'https://...'
}
// 用PUT替换整个用户
const putRequest = {
name: 'Bob',
email: 'bob@example.com'
// 缺少age和avatar字段
};
// 结果:age和avatar可能被删除,用户数据变成
// { id: 1, name: 'Bob', email: 'bob@example.com' }
// 用PATCH只改邮箱
const patchRequest = {
email: 'newemail@example.com'
};
// 结果:只改邮箱,其他字段保持不变
// { id: 1, name: 'Alice', email: 'newemail@example.com', age: 25, avatar: 'https://...' }
// ❌ 初级开发者的"标准错误"
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(data => setUsers(data));
}); // ← 缺少dependency array!
}
// 会发生什么?
// 1. 组件mount → render → useEffect运行 → setUsers
// 2. setUsers触发重新render
// 3. 重新render → useEffect又运行
// 4. setUsers → 重新render
// 5. ... 无限循环
这个bug在React Strict Mode下会立即显现(因为它故意运行两次effect),但在生产环境中可能隐藏一段时间才爆发。
// ✅ 修复:添加dependency array
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(data => setUsers(data));
}, []); // ← 空数组:只在mount时运行一次
}
// ❌ 常见错误:依赖数组不完整
function UserSearch({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => setUser(data));
}, []); // ← 遗漏了userId!
}
// 会怎样?
// 1. 初始化时userId=123,fetch /api/users/123
// 2. userId变成456(比如路由参数改变)
// 3. effect不会重新运行(因为依赖数组是空的)
// 4. 界面还在显示userId=123的数据 ❌
// ✅ 正确做法:包含所有依赖变量
function UserSearch({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => setUser(data));
}, [userId]); // ← 包含userId
// 现在:userId变化 → effect重新运行 → 获取新数据
}
// ❌ 隐藏的内存泄漏代码
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(r => r.json())
.then(data => setUser(data)); // ← 问题在这里
}, []);
// 假设用户快速导航,组件卸载
// 但fetch请求仍在进行,完成后会调用setUser
// 组件已经卸载,调用setState会产生:
// "Cannot perform a React state update on an unmounted component"
}
// ✅ 修复:用cleanup函数
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
let isMounted = true; // 标记组件是否还挂载
fetch('/api/user')
.then(r => r.json())
.then(data => {
// 只有组件还挂载才更新state
if (isMounted) {
setUser(data);
}
});
// cleanup函数:组件卸载时执行
return() => {
isMounted = false;
};
}, []);
}
// ✅✅ 更现代的做法:用AbortController
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch('/api/user', { signal: controller.signal })
.then(r => r.json())
.then(data => setUser(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
// cleanup:组件卸载时取消请求
return() => {
controller.abort(); // 立即停止请求
};
}, []);
}
// ❌ 代码看起来没问题,但有隐藏的竞态条件
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
// 用户在搜索框快速输入 "React"
// query变成:"R" → "Re" → "Rea" → "Reac" → "React"
// 每次都会发送请求
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => setResults(data));
}, [query]);
// 问题:假设延迟
// - 请求1(q=R)延迟500ms
// - 请求2(q=Re)延迟100ms → 先返回
// - 请求3(q=React)延迟300ms → 后返回
// 结果:最后显示的是请求1的结果(过期数据)
}
// ✅ 修复:用cleanup函数控制竞态
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let isCurrentRequest = true; // 标记这个请求是否还"有效"
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
// 只有这个请求仍然是最新的,才更新结果
if (isCurrentRequest) {
setResults(data);
}
});
return() => {
isCurrentRequest = false; // 新的effect运行时,旧请求作废
};
}, [query]);
}
// ✅✅ 最佳实践:防抖 + 竞态保护
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
// 防抖:用户停止输入300ms后才发送请求
const timeoutId = setTimeout(() => {
let isCurrentRequest = true;
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
if (isCurrentRequest) {
setResults(data);
}
});
return() => {
isCurrentRequest = false;
};
}, 300);
return() => clearTimeout(timeoutId);
}, [query]);
}
// ❌ 这样写不行
function UserData() {
const [user, setUser] = useState(null);
useEffect(async () => { // ← useEffect本身不能是async
const response = await fetch('/api/user');
setUser(await response.json());
}, []);
}
// 为什么?useEffect的返回值必须是cleanup函数(或undefined)
// 如果useEffect是async,它返回Promise,这会被React忽略
// cleanup函数就失效了
// ✅ 正确做法1:在useEffect内部定义async函数
function UserData() {
const [user, setUser] = useState(null);
useEffect(() => {
asyncfunction fetchUser() {
const response = await fetch('/api/user');
setUser(await response.json());
}
fetchUser();
}, []);
}
// ✅ 正确做法2:用IIFE
function UserData() {
const [user, setUser] = useState(null);
useEffect(() => {
(async () => {
const response = await fetch('/api/user');
setUser(await response.json());
})();
}, []);
}
最终,你会想要一个可复用的Hook来处理所有这些陷阱:
// ✅ 生产级别的useFetch hook
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return; // 如果URL为空,不发送请求
let isMounted = true;
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
thrownewError(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (isMounted) {
setData(result);
}
} catch (err) {
if (err.name !== 'AbortError' && isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
// cleanup函数
return() => {
isMounted = false;
controller.abort();
};
}, [url, options]);
return { data, loading, error };
}
// 使用示例
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return<div>加载中...</div>;
if (error) return<div>错误: {error}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
但这个hook还不完美。真正的生产环境需要更多功能:
// ✅✅ 企业级useFetch hook
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [refetch, setRefetch] = useState(0); // 用于手动刷新
useEffect(() => {
if (!url) return;
let isMounted = true;
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, options.timeout || 10000);
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
thrownewError(
`HTTP ${response.status}: ${response.statusText}`
);
}
const result = await response.json();
if (isMounted) {
setData(result);
setError(null);
}
} catch (err) {
if (err.name !== 'AbortError' && isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
return() => {
isMounted = false;
controller.abort();
clearTimeout(timeoutId);
};
}, [url, refetch, options]);
return {
data,
loading,
error,
refetch: () => setRefetch(prev => prev + 1) // 手动刷新
};
}
如果你发现自己在写很多这样的代码,就应该考虑用React Query或SWR:
// 使用TanStack Query(React Query)
import { useQuery } from'@tanstack/react-query';
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('/api/users');
if (!res.ok) thrownewError('Failed to fetch');
return res.json();
},
staleTime: 1000 * 60 * 5, // 5分钟
gcTime: 1000 * 60 * 10, // 10分钟后清理
});
if (isLoading) return<div>加载中...</div>;
if (error) return<div>错误</div>;
return<div>{data?.length} 个用户</div>;
}
这样做的好处:
自己写useFetch:
├─ 需要手动处理所有陷阱
├─ 需要手动处理缓存
├─ 需要手动处理重试
├─ 需要手动处理并发
└─ 代码行数:100+
用React Query:
├─ 开箱即用的缓存
├─ 自动去重和合并请求
├─ 内置智能重试
├─ 内置加载状态管理
├─ 代码行数:10-20
如果这篇文章对你有启发,强烈推荐关注微信公众号《前端达人》。每周我们会分享:
📚 深度技术文章
🔧 实战工具和最佳实践
💡 行业洞察
这篇文章帮到你了吗?
一个问题给你思考:你遇到过最诡异的useEffect bug是什么?在评论区分享,我可能会在下一篇文章中深入讨论你的案例。