React 中的副作用处理:掌握 useEffect Hook 的依赖数组、清理函数与正确使用时机

发布时间:2026/9/15 16:26:10
React 中的副作用处理:掌握 useEffect Hook 的依赖数组、清理函数与正确使用时机 React 中的副作用处理掌握 useEffect Hook 的依赖数组、清理函数与正确使用时机【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculumuseEffect是 React 函数组件中处理副作用Side Effect的核心 Hook本指南以 cu/curriculum 仓库中 React 课程的状态与副作用章节 为主体结合仓库内配套课程的源码级示例系统讲解什么是副作用、useEffect的三大组成部分回调函数、依赖数组、清理函数以及什么时候不该用 effect这一关键判断能力。学完本篇你将能够正确地在组件中执行定时器、数据请求、DOM 同步等副作用并避免新手最容易踩的无限循环与多余 effect陷阱。什么是副作用Side Effect在 React 中某些组件需要与自身之外的事物交互。这些外部事物可以是从服务器查询数据例如拉取用户消息列表查找或改变组件在网页上的位置在必要时向服务器发送数据。这种与外部世界的交互被称为副作用side effect。渲染代码和事件处理器并不足以覆盖所有场景。例如当你想连接服务器并获取消息展示给用户时效果effects可以让代码在渲染时或响应式状态值变化时同步你的组件而不是在某个特定事件发生时执行。正如我们有useStateHook 一样React 也提供了便捷的useEffectHook 来在组件中使用效果。在继续之前建议先回顾 useState 与重渲染机制 一课——组件每次状态变化都会触发重渲染这正是副作用失控问题的根源。useEffect 出场修复失控的 Clock 组件问题代码setInterval 直接写在组件体内考虑一个Clock组件它要显示自网页加载以来经过了多少秒。很自然地我们会想到用setInterval每秒给counter状态变量加一。但如果把它直接放在组件函数体里import { useState } from react; export default function Clock() { const [counter, setCounter] useState(0); setInterval(() { setCounter(count count 1) }, 1000); return ( p{counter} seconds have passed./p ); }结果计数器会发疯式地狂涨。原因在于setInterval不是只调用一次而是在每一次状态渲染时都被调用组件首次渲染调用初始的setInterval该定时器每秒更新状态触发组件重渲染每次重渲染又调用一次setInterval产生更多更频繁的状态更新每个新定时器又各自触发新的重渲染……最终彻底失控。这背后的机制与 more_on_state.md 中提到的意外的无限循环警告如出一辙——在组件函数体顶层直接调用setState或安排状态更新会触发渲染 → 更新 → 渲染的恶性循环。用 useEffect 把计算移出渲染流程useEffect此时登场救场它接受一个包含所有计算的回调函数将该计算移出渲染计算流程import { useEffect, useState } from react; export default function Clock() { const [counter, setCounter] useState(0); useEffect(() { setInterval(() { setCounter(count count 1) }, 1000); }) return ( p{counter} seconds have passed./p ); }注意这里仍然在每次渲染后创建新的定时器所以计数依然增长过快。这引出useEffect的第二个参数——依赖数组dependency array。依赖数组控制 effect 何时运行默认情况下useEffectHook 在每一次渲染后都会执行。由于设置状态会让组件重新渲染每次渲染我们依然会新建定时器问题并没有解决。好在第二个参数接受一个依赖数组允许 Hook仅当这些依赖发生变化时才重新运行。如果你有一个状态变量并希望状态每次变化时都触发某个副作用就可以在依赖数组中列出该状态变量。在我们的例子中传入空数组因为我们只希望useEffect在组件首次渲染时运行一次import { useEffect, useState } from react; export default function Clock() { const [counter, setCounter] useState(0); useEffect(() { setInterval(() { setCounter(count count 1) }, 1000); }, []) return ( p{counter} seconds have passed./p ); }依赖数组的三种典型形态总结依赖数组的不同写法与对应行为useEffect(() { // 每次渲染后都会运行 }); useEffect(() { // 仅在挂载时运行组件出现时 }, []); useEffect(() { // 在挂载时运行且当 a 或 b 相比上一次渲染发生变化时也会运行 }, [a, b]);依赖管理的最佳实践通常你不需要手动为useEffect添加依赖——linter如eslint-plugin-react-hooks的exhaustive-deps规则会提示它期望的依赖。让 linter 报错并修复而不是压制它通常是更好的选择。这也是本仓库 JavaScript 课程中的 Linting 一课 所倡导的工程实践让工具帮你守住 Hook 规则的底线。清理函数处理 StrictMode 下的定时器泄漏计数器不再失控了但还剩下一个问题计数器每秒钟会更新两次。这可以理解为 React StrictMode 的行为所致——它本意是帮助我们捕捉 bug那这里的 bug 是什么呢在StrictMode下App组件会被挂载、卸载、再挂载该行为仅存在于开发环境。注意每次useEffect运行时都会新建一个setInterval当组件第一次被卸载时setInterval并没有被停止它依然在累加。这种不必要的残留行为可以通过在组件卸载时清除定时器来避免——这正是useEffect的第三部分清理函数cleanup function。你可以在useEffect的回调中返回一个函数它会在下一次 effect 运行之前执行一次并在组件卸载时最后执行一次。对本例用清理函数清除定时器import { useEffect, useState } from react; export default function Clock() { const [counter, setCounter] useState(0); useEffect(() { const key setInterval(() { setCounter(count count 1) }, 1000); return () { clearInterval(key); }; }, []) return ( p{counter} seconds have passed./p ); }终于正常了关于 StrictMode 的双渲染现象CV 应用项目 中也有专门说明开发环境下部分代码如 console.log会执行两次这是刻意为之的预期行为目的是暴露像上述定时器未清理这样的隐患。useEffect 的完整结构至此我们可以总结useEffectHook 的完整语法useEffect( () { // 执行副作用 return () { // 组件卸载或 effect 重新运行时的清理函数 } }, // 可选的依赖数组 [/* 0 个或多个条目 */] )清理函数的更多实战场景清理函数绝不只用于清除定时器。在 refs_and_memoization.md 一课中它被用来清除setTimeoutuseEffect(() { buttonRef.current.focus(); buttonRef.current.textContent Hey, Im different!; let timeout setTimeout(() { buttonRef.current.textContent Click Me!; }, 2000); return () { clearTimeout(timeout); }; }, []);凡是订阅类操作——定时器、网络请求、事件监听、WebSocket 连接——几乎都需要配套的清理逻辑否则就会在组件卸载后继续占用资源或产生状态泄漏。useEffect 与类组件生命周期方法的对应如果你接触过类组件会发现useEffect本质上是componentDidMount、componentDidUpdate、componentWillUnmount三个生命周期方法的组合详见 component_lifecycle_methods.mduseEffect 写法对应生命周期方法空依赖数组[]componentDidMount带依赖的数组[a, b]componentDidMount 依赖变化时的componentDidUpdate不传依赖数组componentDidMount 每次更新的componentDidUpdate回调中返回清理函数对应componentWillUnmount例如useEffect(() { placeholderFunction(); return () cleanupFunction(); }, [])这段代码只包含componentDidMount空依赖数组和componentWillUnmount返回的清理函数的功能不包含componentDidUpdate。但我们需要 effect 吗——判断该不该用的唯一问题useEffect是 React 常规概念之外的一种机制用于将组件与各种外部系统服务器、API、浏览器 DOM同步。在使用 effect 之前你可以问自己唯一一个问题除了 props 和 state 之外是否有需要同步的外部系统如果答案是否定的那么不必要的useEffect就是代码坏味道code-smell、容易引入错误还会造成不必要的性能开销。下面梳理几个不需要使用 effect 的典型场景。场景一仅基于 state 在渲染期间计算如果你只是根据渲染期间的状态计算某个值不需要 effect。组件因 props 变化而改变时可以在渲染期间直接计算并设置。import { useState } from react; export default function AdditionDisplay() { const [number1, setNumber1] useState(0); const [number2, setNumber2] useState(0); // 以下全部是不必要的 // const [sum, setSum] useState(0); // useEffect(() { // setSum(number1 number2); // }, [number1, number2]); const sum number1 number2; return ( p{number1} {number2} {sum}/p ); }这一原则与 more_on_state.md 中的状态结构建议完全一致不要把可以用现有值、state 和/或 props 计算出来的值放进 state。sum是可推导的派生值直接在渲染中计算即可。场景二事件处理不该交给 effect不需要用 effect 处理事件。组件显示时运行的代码才应该放进 effect其余代码应放在事件处理器中。import { useState } from react; export default function App() { const [input, setInput] useState(); const handleInput (e) { setInput(e.target.value); }; // 非必要时应避免直接操作 DOM // useEffect(() { // document.getElementById(name).addEventListener(change, handleInput); // return () { // document.getElementById(name).removeEventListener(change, handleInput); // } // }); return ( {/* input idname / */} input onChange{handleInput} value{input} / p{ input }/p / ); }把用户输入这类交互放进 effect意味着要手动做 DOM 查询与监听器的增删既绕开了 React 的声明式模型又极易遗漏清理。正确的做法是直接使用受控组件的onChange。场景三基于条件重置 state——用 key 而非 effect大多数情况下你不需要用 effect 来根据某个条件重置状态。你已经在课程中学过 key 的用法——就像在列表项上使用 key 一样给组件添加一个基于应重置状态的 key会为状态值的每次变化创建该组件的独立版本从而自动完成重置。场景四父子组件共享状态——提升 stateLifting State Up如果管理状态遇到困难、想用 effect 去更新父组件或其他非子组件non-child component的状态请考虑提升 statelifting the state。React 中的状态是单向流动的一般沿 DOM 向下传递因此父组件在把数据传给子组件之前就知道了这些数据。如果多个子组件需要使用同一个状态应该把它上移到包含所有这些组件的父组件中而不是使用 effect 这样的逃生舱口。这也与 fetching_data_in_react.md 中把请求提升到组件树上层、通过 props 向下传递响应的思路一脉相承——提升数据源而非滥用 effect 同步。课程中的 effect 实战从数据请求到项目应用在挂载时用 effect 发起 fetchuseEffect最常见的实战用途之一是组件挂载时请求外部 API 数据。fetching_data_in_react.md 中的Image组件完整展示了这一模式用useState管理imageURL、error、loading三个状态用空依赖数组的useEffect只请求一次const Image () { const [imageURL, setImageURL] useState(null); const [error, setError] useState(null); const [loading, setLoading] useState(true); useEffect(() { fetch(https://picsum.photos/v2/list, { headers: { User-Agent: the-odin-project } }) .then((response) { if (response.status 400) { throw new Error(server error); } return response.json(); }) .then((response) setImageURL(response[0].download_url)) .catch((error) setError(error)) .finally(() setLoading(false)); }, []); if (loading) return pLoading.../p; if (error) return pA network error was encountered/p; return ( h1An image/h1 img src{imageURL} alt{placeholder text} / / ); };这里可以观察到三个关键点空依赖数组保证请求只在挂载时发出一次状态更新setImageURL、setError、setLoading发生在异步回调中属于响应外部系统的典型副作用完整的loading/error分支确保用户体验不被网络波动破坏。在课程项目中应用 useEffect记忆卡牌项目要求在组件挂载时调用打乱卡牌顺序的函数这正是空依赖数组useEffect的标准用法同时项目要求从外部 API 获取图片数据同样需要 effect 承载请求。CV 应用项目若你在开发中看到 console.log 等代码执行了两次不要惊慌这是StrictMode的预期行为正好可以利用它检查自己的 effect 是否遗漏了清理函数。知识自检回顾本课核心要点检验自己的理解什么是 effect组件与自身之外的外部系统服务器、API、浏览器 DOM 等进行同步的机制。一个useEffectHook 由什么构成副作用回调函数、可选的依赖数组、可选的清理函数返回值三部分。判断何时使用 effect 的那一个问题是什么除了 props 和 state 之外是否有需要同步的外部系统什么是提升 state把多个子组件共享的状态上移到共同的父组件中让状态沿 DOM 单向向下传递而不是用 effect 这类逃生舱口去横向同步。掌握useEffect的关键不在于记住 API而在于建立两种直觉什么时候副作用需要被 effect 承接定时器、请求、DOM 同步什么时候不该用 effect派生计算、事件处理、条件重置、跨组件状态共享。前者保证你的组件与外部世界正确同步后者保证你的代码简洁、可预测、无性能隐患。后续课程中你还会看到 effect 与useRef、useMemo、useCallback等 Hook 的组合用法它们共同构成了 React 函数组件完整的能力体系。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考