当前位置: 首页 > news >正文

写好 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 的关键是掌握 “同步思维” 而非 “生命周期思维”

  1. 明确同步目标 - 知道你在同步什么
  2. 完整声明依赖 - 信任但不盲从 ESLint
  3. 及时清理资源 - 防止内存泄漏
  4. 保持单一职责 - 一个 Effect 做一件事
  5. 优化依赖关系 - 在完整的基础上精简
  6. 处理竞态条件 - 异步操作的安全性

遵循这些原则,你就能写出可靠、可维护、高性能的副作用代码!

http://www.dtcms.com/a/495647.html

相关文章:

  • 「日拱一码」121 多组学因素分析MOFA
  • Bootstrap 多媒体对象
  • 茶叶网站规划河南省建设工程招标投标协会网站
  • Bootstrap 输入框组
  • 做国内电影网站赚钱不一个空间能放几个网站
  • 带高度多边形,生成3D建筑模型,支持多种颜色或纹理的OBJ、GLTF、3DTiles格式
  • JSP 自定义标签
  • 高效压缩 PDF 文件大小(3 大实用的 Python 库)
  • yum安装的一些问题
  • 网站建设需要的资料库存管理
  • LangChain PromptTemplate 全解析:从模板化提示到智能链构
  • debug - MCUXpresso - 将NXP工程编译过程的所有命令行参数找出来
  • 基于MATLAB的多棵树分类器(随机森林)
  • 瑞芯微RK3588平台FFmpeg硬件编解码移植及性能测试实战攻略
  • 外贸做企业什么网站wordpress在线计算程序
  • 开发个网站开票名称是什么网站的策划分析
  • swift不同的语言环境使用不同的AppName,CFBundleDisplayName
  • php建站程序合肥网站建设讯息
  • 创建网站需要准备哪些资料广汉网站建设ghxhwl
  • SD comfy:教程1
  • 第三十六篇|东方国际学院的教育数据建模实践:首都圈日本语学校的费用结构、生源分布与治理参数分析
  • 保障数据采集稳定性:设计针对淘宝 API 的熔断、降级与重试机制
  • 物流公司网站方案wordpress 文章链接
  • 题解:P14063 [PO Final 2022] 海滩 / Badstrand
  • 虎扑的网站是用什么技术做的短视频推广
  • 潍坊 网站六安网站排名优化电话
  • 编程猫官方网站济南 网站推广
  • macOS 常用快捷键
  • @JsonProperty 注解详解
  • 【系统分析师】高分论文:原型法及其在信息系统开发中的应用