从‘定时器’到‘数据请求’:用5个真实案例彻底搞懂React useEffect的清理函数
从‘定时器’到‘数据请求’用5个真实案例彻底搞懂React useEffect的清理函数在React开发中useEffect可能是最常用但也最容易出错的Hook之一。特别是它的清理机制很多开发者要么完全忽略要么在不该使用的地方滥用。想象一下这样的场景用户在搜索框中快速输入时前一个请求还未完成就发起了新请求或者组件卸载后定时器仍在后台运行导致内存泄漏。这些问题都源于对useEffect清理函数的理解不足。1. 定时器陷阱为什么你的setInterval会内存泄漏让我们从一个最常见的场景开始。假设我们需要在用户进入页面后每隔1秒更新一次计数器function Counter() { const [count, setCount] useState(0); useEffect(() { const timer setInterval(() { setCount(c c 1); }, 1000); }, []); return div{count}/div; }这段代码看起来没问题但实际上存在严重的内存泄漏。当组件卸载时定时器仍在运行继续尝试更新已经卸载的组件状态。正确的做法是useEffect(() { const timer setInterval(() { setCount(c c 1); }, 1000); return () clearInterval(timer); // 清理定时器 }, []);关键点清理函数在组件卸载时执行即使依赖数组为空清理函数也会在每次重新渲染前执行对于setInterval清理函数确保不会在组件不存在时仍更新状态2. 搜索框防抖如何取消未完成的请求在实现搜索功能时我们通常会添加防抖来避免频繁请求。但如果在请求完成前用户就离开了页面会发生什么function SearchBox() { const [query, setQuery] useState(); const [results, setResults] useState([]); useEffect(() { const handler setTimeout(() { fetch(/api/search?q${query}) .then(res res.json()) .then(data setResults(data)); }, 500); return () clearTimeout(handler); // 取消未完成的请求 }, [query]); return ( div input value{query} onChange{e setQuery(e.target.value)} / ul {results.map(item ( li key{item.id}{item.name}/li ))} /ul /div ); }这个案例展示了清理函数不仅可以用于定时器还能用于取消异步操作当query变化时前一个setTimeout会被自动取消避免setState on unmounted component警告3. 事件监听为什么你的resize事件会累积监听全局事件是另一个需要清理的常见场景。考虑一个需要响应窗口大小变化的组件function ResponsiveComponent() { const [width, setWidth] useState(window.innerWidth); useEffect(() { const handleResize () { setWidth(window.innerWidth); }; window.addEventListener(resize, handleResize); return () { window.removeEventListener(resize, handleResize); }; }, []); return div当前宽度: {width}px/div; }如果不添加清理函数每次组件重新挂载都会添加新的事件监听器旧监听器不会被自动移除导致内存泄漏和性能问题4. React Router中的数据请求避免竞态条件在使用React Router时快速切换路由可能导致前一个路由的请求完成后更新错误组件的状态function UserProfile({ userId }) { const [user, setUser] useState(null); useEffect(() { let isActive true; fetch(/api/users/${userId}) .then(res res.json()) .then(data { if (isActive) setUser(data); }); return () { isActive false; // 标记请求为不活跃 }; }, [userId]); if (!user) return div加载中.../div; return ( div h1{user.name}/h1 p{user.bio}/p /div ); }这种竞态条件解决方案使用isActive标志避免更新卸载的组件比直接取消fetch更灵活fetch API本身不支持取消适用于任何异步操作5. 第三方库集成正确销毁ECharts实例与第三方库集成时手动清理尤为重要。以ECharts为例function Chart({ data }) { const chartRef useRef(null); useEffect(() { const chart echarts.init(chartRef.current); const option { // ...基于data的配置 }; chart.setOption(option); return () { chart.dispose(); // 销毁图表实例 }; }, [data]); return div ref{chartRef} style{{ width: 100%, height: 400px }} /; }第三方库通常需要显式清理图表库会创建DOM元素和事件监听器不清理会导致内存泄漏和性能问题确保每次data变化时重新初始化图表清理函数的最佳实践通过这5个案例我们可以总结出一些通用原则何时需要清理定时器/延时器事件监听器网络请求第三方库实例任何需要手动释放的资源清理函数执行时机组件卸载时依赖项变化导致effect重新执行前不会在初始渲染时执行常见错误模式忘记清理导致内存泄漏在不必要时清理如纯计算操作清理函数中使用了过期闭包高级技巧useEffect(() { const abortController new AbortController(); fetch(url, { signal: abortController.signal }) .then(/* ... */); return () abortController.abort(); }, [url]);使用AbortController真正取消fetch请求比isActive标志更彻底在实际项目中我习惯在每个useEffect写完后立即考虑是否需要清理函数。这个简单的习惯能避免大多数与副作用相关的问题。