这是React 18 开发环境下的经典现象:useEffect 在开发模式下会执行两次。
1. 原因分析
React 18 的严格模式(Strict Mode)
在开发环境中,React 18 的严格模式 会故意在组件挂载时:
-
挂载组件
-
卸载组件
-
重新挂载组件
这样做的目的是帮助发现副作用问题(如缺少清理函数、不纯的副作用等)。
// React 18 开发环境执行顺序
useEffect(() => {
console.log('执行'); // 第一次执行
return () => {
console.log('清理'); // 立即清理
};
}, []);
// 然后重新执行一次
// 所以会看到:执行 → 清理 → 执行
2. 解决方案
方案1:添加清理函数(推荐)
确保 useEffect 返回清理函数,避免重复请求造成的问题:
useEffect(() => {
let isMounted = true; // 防止组件已卸载后设置状态
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const data = await response.json();
if (isMounted) {
setData(data);
}
} catch (error) {
if (isMounted) {
setError(error);
}
}
};
fetchData();
return () => {
isMounted = false; // 清理时标记为 false
};
}, []);
方案2:使用请求取消机制
使用 AbortController 取消重复请求:
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
const response = await fetch('/api/data', {
signal: abortController.signal
});
const data = await response.json();
setData(data);
} catch (error) {
if (error.name !== 'AbortError') {
setError(error);
}
}
};
fetchData();
return () => {
abortController.abort(); // 取消请求
};
}, []);
方案3:使用 useRef 跳过第一次执行
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
// 第二次及以后才执行
fetchData();
}, []);
方案4:使用 React Query / TanStack Query(推荐)
这是最优雅的方案,自动处理重复请求、缓存、重试等:
下载插件:
npm install @tanstack/react-query
import { useQuery } from '@tanstack/react-query';
function MyComponent() {
const { data, isLoading, error } = useQuery({
queryKey: ['myData'],
queryFn: () => fetch('/api/data').then(res => res.json())
});
// 自动处理重复请求,开发环境也只发一次
}
六、总结
| 方案 | 优点 | 适用场景 |
|---|---|---|
| 清理函数 + isMounted | 简单直接 | 小型项目,简单请求 |
| AbortController | 取消重复请求 | 需要取消请求的场景 |
| React Query | 功能完整,自动处理 | 中大型项目,数据密集型 |