写好 React useEffect 的终极指南
🎯 核心心法:转变思维模式
从生命周期到副作用同步
- 旧思维:“在组件挂载时执行这个副作用”
- 新思维:“当这些依赖项变化时,我需要同步外部系统与组件状态”
useEffect
的本质:在渲染后,根据依赖项将外部系统与当前 props/state 同步
⚡ 五大核心原则
1. 详尽的依赖项数组
规则:所有在 effect 内部使用的外部值都必须声明为依赖
// ❌ 依赖缺失 - 闭包陷阱
function ProductPage({ productId }) {const [product, setProduct] = useState(null);useEffect(() => {fetch(`/api/products/${productId}`).then(setProduct);}, []); // 缺少 productIdreturn <div>{product?.name}</div>;
}// ✅ 依赖完整
function ProductPage({ productId }) {const [product, setProduct] = useState(null);useEffect(() => {fetch(`/api/products/${productId}`).then(setProduct);}, [productId]); // 所有依赖都声明return <div>{product?.name}</div>;
}
启用 ESLint 规则:这是你最好的朋友!
2. 单一职责原则
每个 useEffect 只负责一件事
// ❌ 混合职责
function UserProfile({ userId }) {const [user, setUser] = useState(null);const [theme, setTheme] = useTheme();useEffect(() => {fetchUser(userId).then(setUser); // 职责1:数据获取document.title = `Profile`; // 职责2:DOM操作 document.body.className = theme; // 职责3:样式应用}, [userId, theme]);// ...
}// ✅ 职责分离
function UserProfile({ userId }) {const [user, setUser] = useState(null);const [theme, setTheme] = useTheme();// Effect 1:只负责数据获取useEffect(() => {fetchUser(userId).then(setUser);}, [userId]);// Effect 2:只负责文档标题useEffect(() => {if (user) {document.title = `Profile of ${user.name}`;}}, [user]);// Effect 3:只负责主题应用useEffect(() => {document.body.className = theme;}, [theme]);// ...
}
3. 提供清理函数
清理时机:组件卸载时 + 下次 effect 执行前
function EventListenerComponent() {const [position, setPosition] = useState({ x: 0, y: 0 });useEffect(() => {const handleMouseMove = (event) => {setPosition({ x: event.clientX, y: event.clientY });};// 1. 添加事件监听window.addEventListener('mousemove', handleMouseMove);// ✅ 返回清理函数return () => {// 2. 移除事件监听window.removeEventListener('mousemove', handleMouseMove);};}, []); // 空依赖:只在挂载时执行return <div>Position: {position.x}, {position.y}</div>;
}
需要清理的资源:
- 事件监听器
- 定时器
- WebSocket 连接
- 订阅
- 异步操作
4. 减少不必要的依赖
在依赖完整的前提下优化
技巧 1:函数式更新 State
// ❌ 依赖 count
useEffect(() => {const id = setInterval(() => {setCount(count + 1); // 需要 count 依赖}, 1000);return () => clearInterval(id);
}, [count]);// ✅ 无依赖
useEffect(() => {const id = setInterval(() => {setCount(c => c + 1); // 函数式更新}, 1000);return () => clearInterval(id);
}, []);
技巧 2:函数移入 Effect 内部
// ❌ 外部函数需要作为依赖
const fetchData = () => { /* ... */ };
useEffect(() => {fetchData();
}, [fetchData]);// ✅ 函数在内部,无需依赖
useEffect(() => {const fetchData = () => { /* ... */ };fetchData();
}, []);
技巧 3:使用 useCallback/useMemo 稳定引用
// ✅ 稳定函数引用
const fetchProduct = useCallback(() => {// ...
}, [productId]); // 只有 productId 变化时重新创建useEffect(() => {fetchProduct();
}, [fetchProduct]); // 现在依赖是稳定的
5. 处理竞态条件
防止过时数据覆盖最新数据
function DataFetcher({ id }) {const [data, setData] = useState(null);useEffect(() => {let ignore = false; // 忽略标志const fetchData = async () => {const result = await fetch(`/api/data/${id}`);const jsonData = await result.json();if (!ignore) { // 只有不忽略时才更新状态setData(jsonData);}};fetchData();return () => {ignore = true; // 清理时设置忽略标志};}, [id]); // id 变化时取消上一次请求return <div>{data?.name}</div>;
}
🛠️ 常用代码模板
模板 1:数据获取(完整版)
useEffect(() => {let ignore = false;const fetchData = async () => {try {setLoading(true);setError(null);const response = await fetch(`/api/data/${id}`);if (!response.ok) throw new Error('Network error');const result = await response.json();if (!ignore) {setData(result);}} catch (err) {if (!ignore) {setError(err.message);}} finally {if (!ignore) {setLoading(false);}}};fetchData();return () => {ignore = true;};
}, [id]);
模板 2:事件监听器
useEffect(() => {const handleKeyPress = (event) => {if (event.key === 'Escape') {onClose();}};document.addEventListener('keydown', handleKeyPress);return () => {document.removeEventListener('keydown', handleKeyPress);};
}, [onClose]); // onClose 需要用 useCallback 包裹
模板 3:定时器
useEffect(() => {const intervalId = setInterval(() => {setCount(prev => prev + 1); // 函数式更新,无依赖}, 1000);return () => clearInterval(intervalId);
}, []); // 空依赖:定时器只创建一次
模板 4:第三方库集成
useEffect(() => {const chart = new ChartJS(ctx, {type: 'line',data: chartData,options: chartOptions});return () => {chart.destroy(); // 清理第三方库实例};
}, [chartData, chartOptions]); // 数据变化时重新创建
模板 5:表单验证
useEffect(() => {const errors = validateForm(formState);setFormErrors(errors);
}, [formState]); // formState 变化时重新验证
🔧 高级模式
自定义 Hook 封装
// 自定义数据获取 Hook
function useApiData(url) {const [data, setData] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {let ignore = false;const fetchData = async () => {try {setLoading(true);const response = await fetch(url);const result = await response.json();if (!ignore) {setData(result);}} catch (err) {if (!ignore) {setError(err.message);}} finally {if (!ignore) {setLoading(false);}}};fetchData();return () => { ignore = true; };}, [url]);return { data, loading, error };
}// 使用
function UserProfile({ userId }) {const { data: user, loading, error } = useApiData(`/api/users/${userId}`);if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error}</div>;return <div>{user.name}</div>;
}
依赖项调试
// 调试依赖项变化
useEffect(() => {console.log('Effect running with:', { prop1, state1 });// 副作用逻辑...
}, [prop1, state1]);// 或者使用 useWhatChanged
function useWhatChanged(dependencies, names) {const prevRef = useRef(dependencies);useEffect(() => {dependencies.forEach((dep, i) => {if (dep !== prevRef.current[i]) {console.log(`${names[i]} changed:`, {from: prevRef.current[i],to: dep});}});prevRef.current = dependencies;});
}// 使用
useWhatChanged([prop1, state1], ['prop1', 'state1']);
✅ 检查清单
在编写每个 useEffect 时,问自己:
1. 目的明确
- 这个 Effect 在同步什么外部系统?
- 它解决了什么问题?
2. 依赖完整
- 所有使用的外部值都声明为依赖了吗?
- ESLint 规则通过了吗?
3. 清理必要
- 需要返回清理函数吗?
- 清理了所有创建的资源吗?
4. 职责单一
- 这个 Effect 只做一件事吗?
- 需要拆分成多个 Effect 吗?
5. 依赖优化
- 可以使用函数式更新减少依赖吗?
- 可以使用 useCallback/useMemo 稳定引用吗?
6. 竞态安全
- 异步操作有竞态条件风险吗?
- 使用了忽略标志吗?
7. 性能考虑
- 依赖项变化会导致不必要的执行吗?
- 可以使用条件执行优化吗?
🎯 总结
写好 useEffect
的关键是掌握 “同步思维” 而非 “生命周期思维”:
- 明确同步目标 - 知道你在同步什么
- 完整声明依赖 - 信任但不盲从 ESLint
- 及时清理资源 - 防止内存泄漏
- 保持单一职责 - 一个 Effect 做一件事
- 优化依赖关系 - 在完整的基础上精简
- 处理竞态条件 - 异步操作的安全性
遵循这些原则,你就能写出可靠、可维护、高性能的副作用代码!