核心思路
useEffect 的回调函数本身不能直接是 async 函数(因为会返回 Promise,而 useEffect 期望返回清理函数或 undefined),所以正确的做法是:在 useEffect 内部定义一个异步函数,然后立即调用它。
完整示例代码
下面是几种常见的异步操作场景(以请求接口为例),包含完整的错误处理和清理逻辑:
jsx
import { useEffect, useState } from 'react';
function AsyncEffectDemo() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 1. 定义异步函数
const fetchData = async () => {
try {
setLoading(true);
// 模拟异步请求(比如调用 API)
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false); // 无论成功/失败,都结束加载状态
}
};
// 2. 立即调用异步函数
fetchData();
// 3. 可选:清理函数(防止组件卸载后执行 setState)
return () => {
// 如果是 axios 等库,可以在这里取消请求
// 示例:controller.abort()
console.log('组件卸载/依赖变更,清理异步操作');
};
}, []); // 空依赖数组:仅在组件挂载时执行一次
// 渲染逻辑
if (loading) return <div>加载中...</div>;
if (error) return <div>错误:{error}</div>;
return (
<div>
<h3>异步请求结果:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default AsyncEffectDemo;
关键细节解释
1. 为什么不能直接写 async useEffect?
jsx
// ❌ 错误写法:useEffect 回调返回 Promise,React 会警告
useEffect(async () => {
const data = await fetchData();
}, []);
// ✅ 正确写法:内部定义 async 函数并调用
useEffect(() => {
const asyncFunc = async () => { /* 异步逻辑 */ };
asyncFunc();
}, []);
2. 处理取消请求(避免内存泄漏)
如果异步操作还在进行时组件卸载,可能会导致 setState 警告,推荐用 AbortController 取消请求:
jsx
useEffect(() => {
const controller = new AbortController(); // 创建中止控制器
const signal = controller.signal;
const fetchData = async () => {
try {
const response = await fetch('https://xxx', { signal }); // 关联信号
const data = await response.json();
setData(data);
} catch (err) {
// abort也会抛出异常
// 忽略 "中止" 错误(主动取消请求时触发)
if (err.name !== 'AbortError') setError(err.message);
}
};
fetchData();
// 组件卸载/依赖变更时,取消请求
return () => controller.abort();
}, []);
3. 竞态条件(Race Condition) 问题
先理解什么是 useEffect 中的竞态条件
简单来说,竞态条件指的是:当一个异步请求还未完成时,依赖项发生了变化,触发了新的请求,而旧请求的响应晚于新请求,导致页面最终渲染的是旧请求的数据,覆盖了正确的新数据。
比如:用户快速切换筛选条件(如 userId 从 1 变到 2),userId=1 的请求响应较慢,userId=2 的请求先返回,随后 userId=1 的请求才返回,最终页面显示的是 userId=1 的数据,这就是竞态问题。
核心解决思路
解决的核心是让“过时”的异步操作结果失效,常用的方案有 3 种(按推荐优先级排序):
- 使用
AbortController取消旧请求(最推荐,从源头终止); - 使用“标记变量”忽略旧请求的结果;
- 使用 useRef 追踪最新的请求标识。
解决方案 1:AbortController 取消旧请求(官方推荐)
这是最彻底的方案,直接终止未完成的旧请求,从根源上避免竞态。
jsx
import { useEffect, useState } from 'react';
function RaceConditionDemo() {
const [userId, setUserId] = useState(1);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 1. 创建中止控制器
const controller = new AbortController();
const signal = controller.signal;
const fetchUser = async () => {
setLoading(true);
try {
// 2. 请求时关联 signal
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, { signal });
if (!response.ok) throw new Error('请求失败');
const result = await response.json();
// 3. 只有未被中止的请求,才会更新状态
setData(result);
} catch (err) {
// 忽略主动中止的错误(避免控制台警告)
if (err.name !== 'AbortError') {
console.error('请求错误:', err);
}
} finally {
setLoading(false);
}
};
fetchUser();
// 4. 依赖变化/组件卸载时,取消旧请求
return () => controller.abort();
}, [userId]); // 依赖 userId,变化时触发新请求
return (
<div>
<button onClick={() => setUserId(1)}>用户1</button>
<button onClick={() => setUserId(2)}>用户2</button>
<button onClick={() => setUserId(3)}>用户3</button>
{loading && <div>加载中...</div>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
export default RaceConditionDemo;
方案 2:标记变量忽略旧请求结果(兼容旧环境)
如果你的环境不支持 AbortController(如部分老旧浏览器),可以用一个“是否有效”的标记变量,让旧请求的结果不更新状态。
jsx
useEffect(() => {
// 1. 定义标记变量,标记当前请求是否有效
let isRequestValid = true;
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
const result = await response.json();
// 2. 只有标记为有效时,才更新状态
if (isRequestValid) {
setData(result);
}
} catch (err) {
if (isRequestValid) {
console.error('请求错误:', err);
}
} finally {
if (isRequestValid) {
setLoading(false);
}
}
};
fetchUser();
// 3. 依赖变化/组件卸载时,将标记置为无效
return () => {
isRequestValid = false;
};
}, [userId]);
关键细节说明
- AbortController 的优势:不仅能避免竞态,还能节省网络资源(终止未完成的请求),是 React 官方最推荐的方案;
- 标记变量的注意事项:变量必须定义在 useEffect 内部,确保每次触发都有独立的标记,不能用 useState(会触发额外渲染);
- 竞态的常见场景:
- 快速切换筛选条件/分页;
- 搜索框实时联想(输入过快触发多次请求);
- 组件还在加载时被卸载,又重新挂载。
总结
- 优先方案:使用
AbortController取消旧请求,既解决竞态又节省网络资源; - 兼容方案:用 useEffect 内部的标记变量(如
isRequestValid),忽略旧请求的状态更新; - 核心逻辑:让“过时”的异步操作要么被终止,要么其结果不参与状态更新,确保最终渲染的是最新请求的数据。
这三种方案都能有效解决竞态条件问题,你可以根据项目的环境和复杂度选择最合适的方式。
总结
- 核心规则:
useEffect回调不能直接是 async 函数,需在内部定义 async 函数并立即调用。 - 错误处理:必须用 try/catch 捕获异步操作的错误,避免未处理的 Promise 异常。
- 内存安全:使用
AbortController取消未完成的异步请求,防止组件卸载后执行 setState。 - 竞态条件:使用
AbortController取消未完成的异步请求,防止旧请求数据覆盖新请求。
这是 React 官方推荐的异步操作写法,既符合 Hook 的规则,又能避免内存泄漏和警告问题。