React useEffect 实战技巧:从基础到高级应用

张开发
2026/4/4 7:20:49 15 分钟阅读
React useEffect 实战技巧:从基础到高级应用
1. useEffect 基础从零开始理解副作用处理初次接触 React 的函数组件时很多人会对 useEffect 这个 Hook 感到困惑。其实它的核心思想很简单处理那些与组件渲染无关的操作。想象你正在装修房子刷墙、铺地板是主要工作相当于组件的渲染而清理装修垃圾、联系物业报备这些额外工作就是副作用。最基础的 useEffect 用法是这样的import { useEffect } from react; function MyComponent() { useEffect(() { console.log(组件渲染完成); }); return div示例组件/div; }这段代码会在每次组件渲染后执行包括初次渲染和后续的每次更新。我在实际项目中经常用它来做调试比在 render 方法里直接 console.log 更清晰因为它明确区分了渲染和副作用这两个阶段。初学者常犯的一个错误是忘记添加依赖数组。比如下面这个计时器例子function BuggyTimer() { const [count, setCount] useState(0); useEffect(() { const timer setInterval(() { setCount(count 1); // 这里有问题 }, 1000); return () clearInterval(timer); }, []); // 空依赖数组 return div{count}/div; }这个组件看似能正常工作但实际上计时器永远只会显示1。因为闭包问题count 的值被锁死在初始值0。正确的做法应该使用函数式更新setCount(prevCount prevCount 1);2. 依赖数组的精准控制避免过度渲染依赖数组是 useEffect 最强大的特性之一但也是最容易出错的地方。我把它比作汽车的油门控制不传依赖数组就像油门踩到底每次渲染都执行空数组就像挂空挡只执行一次而精确指定依赖则是定速巡航只在特定变化时执行。常见依赖处理场景只在挂载时执行类组件的componentDidMountuseEffect(() { fetchUserData(); // 初始化数据获取 }, []); // 空数组特定状态变化时执行const [userId, setUserId] useState(1); useEffect(() { fetchUserProfile(userId); // 用户ID变化时重新获取 }, [userId]); // 精确依赖多个依赖组合const [page, setPage] useState(1); const [pageSize, setPageSize] useState(10); useEffect(() { fetchListData(page, pageSize); // 页码或页数变化时刷新 }, [page, pageSize]); // 组合依赖我在项目中遇到过这样一个性能问题一个复杂表单组件在每次按键时都会触发整个页面的重渲染。排查后发现是因为在 useEffect 中错误地依赖了整个 formData 对象// 错误示范 - 会导致性能问题 useEffect(() { autoSave(formData); }, [formData]); // 依赖了整个对象解决方案是只依赖真正需要监听的字段// 正确做法 - 精确依赖 useEffect(() { autoSave({ username: formData.username }); }, [formData.username]); // 只依赖用户名3. 副作用清理防止内存泄漏的关键技巧清理副作用就像离开房间时要关灯一样重要。React 会在组件卸载时自动执行清理函数但很多开发者容易忽略这一点。我曾经接手过一个项目发现切换路由时内存持续增长就是因为没有正确清理事件监听器。典型清理场景示例事件监听清理useEffect(() { const handleScroll () { console.log(window.scrollY); }; window.addEventListener(scroll, handleScroll); return () { window.removeEventListener(scroll, handleScroll); }; }, []);定时器清理useEffect(() { const timer setInterval(() { console.log(心跳检测); }, 5000); return () clearInterval(timer); }, []);订阅清理useEffect(() { const subscription dataStream.subscribe(data { updateState(data); }); return () subscription.unsubscribe(); }, []);一个高级技巧是依赖变化时的清理。React 会在每次重新执行 effect 前先执行上一次的清理函数。利用这个特性我们可以实现依赖变化时的资源释放和重新初始化useEffect(() { const socket createSocketConnection(roomId); socket.on(message, handleMessage); return () { socket.disconnect(); // 切换房间时先断开旧连接 }; }, [roomId]); // 房间变化时重建连接4. 高级模式useEffect 性能优化实战当组件变得复杂时useEffect 的性能优化就显得尤为重要。以下是几个经过实战验证的高级技巧1. 使用 useCallback 避免不必要重执行const fetchData useCallback(async () { const res await api.get(/data, { params }); setData(res.data); }, [params]); // 只有当params变化时才会重新创建函数 useEffect(() { fetchData(); }, [fetchData]); // 现在依赖更稳定了2. 使用 useRef 存储可变值const timerRef useRef(null); useEffect(() { timerRef.current setInterval(() { // 某些操作 }, 1000); return () clearInterval(timerRef.current); }, []);3. 批量状态更新减少渲染次数useEffect(() { // 不好的做法 - 会触发多次渲染 setLoading(true); fetchData().then(data { setData(data); setLoading(false); }); // 好的做法 - 使用状态合并 setLoading(true); fetchData().then(data { setState({ data, loading: false }); }); }, []);4. 使用 useReducer 处理复杂状态逻辑const [state, dispatch] useReducer(reducer, initialState); useEffect(() { dispatch({ type: FETCH_START }); fetchData() .then(data dispatch({ type: FETCH_SUCCESS, payload: data })) .catch(err dispatch({ type: FETCH_ERROR, payload: err })); }, []);在大型项目中我特别推荐使用自定义 Hook来封装复杂的 useEffect 逻辑。比如这个数据获取 Hookfunction useFetch(url, options) { const [state, setState] useState({ data: null, loading: true, error: null }); useEffect(() { let mounted true; setState(s ({ ...s, loading: true })); fetch(url, options) .then(res res.json()) .then(data { if (mounted) { setState({ data, loading: false, error: null }); } }) .catch(error { if (mounted) { setState({ data: null, loading: false, error }); } }); return () { mounted false; }; }, [url, options]); return state; }5. 常见陷阱与解决方案即使是有经验的 React 开发者在使用 useEffect 时也难免会踩坑。以下是几个我亲身经历过的典型问题1. 无限循环陷阱// 危险的写法 - 会导致无限循环 const [count, setCount] useState(0); useEffect(() { setCount(count 1); // 每次effect执行都会改变count }, [count]); // 而count变化又会触发effect解决方案是使用函数式更新或重新设计状态逻辑// 安全写法 useEffect(() { setCount(prev prev 1); // 不依赖外部count值 }, []); // 空依赖2. 过时的闭包值const [count, setCount] useState(0); useEffect(() { const timer setInterval(() { console.log(count); // 总是打印初始值0 }, 1000); return () clearInterval(timer); }, []); // 空依赖解决方法是用 useRef 保持最新值const countRef useRef(count); useEffect(() { countRef.current count; // 每次count变化时更新ref }, [count]); useEffect(() { const timer setInterval(() { console.log(countRef.current); // 现在能获取最新值 }, 1000); return () clearInterval(timer); }, []);3. 竞态条件Race Condition在快速切换数据请求时比如标签页切换可能会出现后发请求先返回的情况useEffect(() { fetch(/api/user/${userId}) .then(res res.json()) .then(data setUser(data)); }, [userId]); // 当快速切换userId时可能出错解决方案是使用取消标记useEffect(() { let didCancel false; fetch(/api/user/${userId}) .then(res res.json()) .then(data { if (!didCancel) { setUser(data); } }); return () { didCancel true; // 当userId变化时取消前一个请求 }; }, [userId]);6. 与其它Hook的配合使用useEffect 很少单独使用与其他Hook配合能发挥更大威力。下面是一些经典组合1. useEffect useMemoconst expensiveValue useMemo(() { return computeExpensiveValue(a, b); }, [a, b]); // 只有当a或b变化时重新计算 useEffect(() { // 使用memoized值 doSomethingWith(expensiveValue); }, [expensiveValue]); // 依赖更稳定2. useEffect useContextconst theme useContext(ThemeContext); useEffect(() { // 当主题变化时调整样式 document.body.className theme; }, [theme]);3. 自定义Hook封装将常用的 useEffect 模式抽象成自定义Hookfunction useWindowSize() { const [size, setSize] useState({ width: window.innerWidth, height: window.innerHeight }); useEffect(() { const handleResize () { setSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener(resize, handleResize); return () window.removeEventListener(resize, handleResize); }, []); return size; }7. 实战案例构建一个健壮的数据获取Hook结合前面所学让我们实现一个生产环境可用的数据获取Hook。这个版本包含加载状态、错误处理和取消逻辑import { useState, useEffect, useRef } from react; function useFetch(url, options {}) { const [state, setState] useState({ data: null, error: null, loading: true }); const abortControllerRef useRef(null); useEffect(() { const fetchData async () { abortControllerRef.current?.abort(); abortControllerRef.current new AbortController(); try { setState(prev ({ ...prev, loading: true })); const res await fetch(url, { ...options, signal: abortControllerRef.current.signal }); if (!res.ok) { throw new Error(HTTP error! status: ${res.status}); } const data await res.json(); setState({ data, error: null, loading: false }); } catch (error) { if (error.name ! AbortError) { setState({ data: null, error: error.message, loading: false }); } } }; fetchData(); return () { abortControllerRef.current?.abort(); }; }, [url, JSON.stringify(options)]); // 注意options的比较方式 return state; }使用示例function UserProfile({ userId }) { const { data: user, loading, error } useFetch( https://api.example.com/users/${userId} ); if (loading) return div加载中.../div; if (error) return div错误: {error}/div; return ( div h2{user.name}/h2 p邮箱: {user.email}/p /div ); }这个实现考虑了以下关键点使用 AbortController 取消未完成的请求正确处理错误状态避免组件卸载后的状态更新提供清晰的加载和错误状态自动重新获取当url或options变化时

更多文章