- useState 和 useEffect 集中了 React 函数式组件的大部分状态和副作用,取代了许多经典的生命周期方法。
- 效果可以包含返回函数的清理逻辑,使您可以管理订阅、间隔和其他外部资源而不会发生内存泄漏。
- 依赖数组控制每个 effect 的执行时间,并且必须包含其中使用的所有响应式值,以平衡正确性和性能。
- 将逻辑拆分成几个小的 useEffect,并按职责进行划分,可以提高可读性,方便重用,并减少无限渲染或不正确的依赖项等常见错误。

如果你每天都使用 React,那么掌握 useState 和 useEffect 就是必不可少的。这几乎是每天都会用到的。这两个 Hook 处理了现代应用程序的大部分状态和副作用逻辑,从简单的计数器到包含 HTTP 请求、事件订阅和动画的复杂界面,无所不能。
问题在于,很容易“凭感觉”使用它们,最终导致内存泄漏、无休止的渲染,或者在不应该触发的时候触发特效。在本文中,我们将冷静地讲解如何正确使用 useState,尤其是 useEffect:它的具体作用、它与组件生命周期的关系、如何管理清理、如何使用依赖数组优化性能,以及如何在不费脑筋的情况下应用高级模式。
什么是 Hooks?为什么 useState 和 useEffect 如此重要?
React 16.8 中引入了 Hooks,允许在无需使用类的情况下使用状态和其他功能。而不是从 React.Component 在处理生命周期方法时,我们会使用调用 Hooks 的函数,例如 useState y useEffect 里面。
useState 是一个 Hook,它允许你在函数组件中获取本地状态。它返回一个值和一个用于更新该值的函数,每次更新该状态时,都会触发组件的新渲染。它是实现界面动态化的基础:计数器、表单、加载标志等等。
另一方面,useEffect 是一个 Hook,它允许你在渲染后执行辅助效果。这包括数据请求、WebSocket订阅、DOM监听器、计时器、与外部库的集成,或者在没有其他选择时手动更新DOM等操作。
如果你之前使用过类组件,你可以把 useEffect 看作是 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合。但所有功能都封装在一个声明式 API 中。其理念是考虑“渲染后发生的效果”,而不是单独的“组装”或“更新”阶段。
useEffect 在概念层面上的工作原理
基本的 useEffect 签名非常简单: useEffect(configuración, dependencias?)你可以向它传递一个配置函数,该函数将在组件绘制完成后运行;还可以选择传递一个依赖项列表,该列表决定何时重复该效果。
配置函数集中了你的效果逻辑:订阅某些内容、发起请求、启动间隔、修改 DOM 等。 该函数还可以返回另一个清理函数,React 将在需要撤销你所做的操作时执行该函数:取消时间间隔、取消订阅、中止请求等。
useEffect 的第二个参数是著名的依赖数组。你需要在这里列出所有在 effect 中使用的“响应式”值:props、state,以及直接在组件主体中定义的任何变量或函数。React 会将该数组与上一次渲染的数组进行比较。 Object.is如果任何依赖项发生更改,请清除旧的 effect 并运行新的 effect。
如果没有传递依赖项数组,useEffect 会在每次渲染后执行。如果你传递一个空数组 []该效果仅在组装时执行一次,拆卸时即被清除,模拟扭矩作用。 componentDidMount / componentWillUnmount如果传递特定的依赖项,则只有当其中一个值发生变化时,效果才会触发。
第一步:结合 useState 和 useEffect
要理解这两个 Hook 之间的关系,一个经典的例子是更新文档标题的典型计数器。。 同 useState 您可以控制点击次数,并且 useEffect 你改变了 document.title 每次渲染后:
关键在于 useEffect 是在组件内部定义的。因此,借助 JavaScript 闭包,您可以直接读取当前状态和属性。您无需使用特殊的 React API 即可在 effect 中访问状态;一切都在函数的作用域内。
此外,React 不会在渲染之间重用同一个 effect 函数:每次渲染都会生成一个新的函数。这是有意为之:每个 effect 都与其创建时的渲染事件相关联,当 React 检测到依赖项发生变化时,它会先清理旧 effect,然后再用新 effect 替换旧 effect。这种模型使得理解每个 effect 在任何给定时间点所看到的值变得更加容易。
另一个重要的细节是,这些特效不会阻碍界面绘制。React 首先更新 DOM 并让浏览器渲染,然后执行 useEffect对于大多数情况(请求), 日志等等。)这可以提升流畅感。如果您需要在用户看到屏幕之前同步执行某些操作(例如,测量并重新定位工具提示),则应该使用 useLayoutEffect它共享 API,但在绘制之前运行。
效果类型:清洁前后的效果
并非所有副作用都一样:有些副作用运行完毕就结束了,而另一些副作用则需要显式清理。区分这两种类型是避免记忆中留下未解决的问题或出现奇怪行为的关键。
免清洗效果是指不留下任何“开放”状态的效果。例如,写入日志、发出一次性请求并将结果保存到状态、启动自管理动画或进行一次性 DOM 修改。就生命周期而言,它们仅依赖于 componentDidMount / componentDidUpdate 他们什么都不要求。 componentWillUnmount.
一个典型的例子是使用 navigator.geolocation.getCurrentPosition 组件渲染完成后你请求位置信息,用经纬度更新状态,React 重新渲染,就完成了。你没有留下永久监听器,所以卸载组件时不需要清理任何东西。
当您的组件持续连接到外部系统时,清洁效果就会显现。这些包括对 WebSocket 或聊天 API 的订阅、DOM 事件监听器、重复间隔或超时等。在这些情况下,如果您在卸载或更改依赖项时不进行清理,最终会导致内存泄漏或意外行为。
React 处理这种清理工作的惯用方法是从 effect 中返回一个函数。此函数会在每次需要“卸载”效果时运行:例如组件从 DOM 中移除,或者依赖项发生更改且需要用新效果替换旧效果时。一个典型的例子是带有计数器的组件。 setInterval 用……清洗 clearInterval 效果回归。
useEffect 应用于全局事件和订阅
useEffect 的一个非常常见的用途是订阅全局浏览器事件。如 resize 该 window o keydown 该 document在这种情况下,您需要在设置组件时连接到事件,并在拆卸组件时断开连接,以避免监听器累积。
模式始终相同:在效果器中添加监听器,并在清理函数中移除它们。此外,如果您希望该订阅只创建一次,则可以传递一个空的依赖项数组。 [] 这样就不会在每次渲染中重复出现。
同样的方法也适用于与外部 API、第三方 SDK 或任何强制您在组件可见时“连接”的系统进行集成。你在 effect 设置函数中连接,在清理函数中断开连接,相信 React 会在依赖项更改时根据需要调用这两个函数。
在开发模式和严格模式下,React 会在挂载后立即执行额外的配置和清理操作。这是一种压力测试,旨在确保你的清理逻辑能够真正撤销配置所做的所有更改。如果你只在开发环境中发现异常行为,通常是因为你的清理工作做得不够完善。
使用 useEffect 处理 HTTP 请求和竞态条件
useEffect 的另一个非常常见的应用场景是,当某些属性或状态的一部分发生变化时,发出 HTTP 请求。例如,当用户数据发生变化时加载该用户数据。 userId 或者当道具中的名称更新时,检索有关宝可梦的信息。
总体思路是:定义一个触发异步函数的效果,用接收到的数据更新状态,并在依赖数组中指定该请求所依赖的值。如果您只想在骑行时触发,请跳过此步骤。 []如果你希望它在每次内容更改时都运行的话。 userId包括 userId 在数组中。
一个需要注意的细节是,如果在请求响应之前移除组件会发生什么情况。. 是的 then 或之后 await 致电 setState 对于不再安装的组件,您可能会遇到警告,或者更糟糕的是,会收到有关僵尸组件的状态更新。
避免这种情况的常见方法是使用内部标志(例如, isMounted o ignore)你在效果中定义它,你把它放进去 true在清洁功能中,您将其设置为 false在异步函数内部,就在调用之前 setState你要检查该标志是否仍然处于开启状态。 true如果已经是 false您跳过了更新步骤。这也有助于避免出现竞态条件,即多个请求的解析顺序可能与提交顺序不同。
另一种更现代的选择是,如果您使用的 API 支持 AbortController,则可以使用它。因此,清理函数会调用 abort() 并且,请求的承诺被拒绝,而没有试图影响状态。无论如何,取消或忽略响应的逻辑必须蕴含在效果本身之中。
同一组件中多次调用 useEffect
Hooks 的一个优点是,它允许你根据逻辑的功能来分离逻辑,而不是根据逻辑需要放置的生命周期方法来分离逻辑。使用多个时没有任何限制。 useEffect 在同一组件中。
这意味着你可以使用一个效果来管理文档标题,另一个效果来订阅聊天,还有一个效果来监听滚动事件。React 会按照组件中出现的顺序,依次执行每个组件及其依赖项和清理逻辑。
与类(class)相比,类中很多不相关的逻辑最终混杂在同一个生命周期方法中,而 Hooks 鼓励你按职责划分。一个效果对应一个特定目的。如果一个效果开始产生多种不同的效果,那么可能就应该将其拆分成几个效果。
当把这些效果封装在自定义钩子中时,这种模式会变得更加强大。 (例如, useWindowSize, useChatStatus等等),但即使不做到那一步,使用多个小的效果而不是一个巨大的效果也能提高可读性。
依赖项列表:响应式和性能
依赖项的集合既是 useEffect 的强大之处,也是许多麻烦的根源。总体规则很简单:在 effect 中使用的每个响应式值都必须出现在该数组中。响应式值包括 props、state 以及组件中定义的变量或函数。
这不是要“选择”依赖项来减少效果的运行时间,而是要诚实地描述效果的逻辑依赖于哪些值。如果短切 eslint-plugin-react-hooks 它配置正确;当您遗漏了某些内容或添加了不必要的内容时,它会发出警报。
如果你想移除一个依赖项,你不能通过消除警告来实现;你需要修改代码,使该值不再具有响应性。例如,如果您使用常量 serverUrl 由于它始终保持不变,您可以将其移出组件主体。这样,它就不再是响应式值,您可以将其从数组中移除而不会误导 React。
当一个效果读取到一个响应值时,你通常希望它能对该响应值的变化做出反应。这意味着将其添加到数组中。但有时您可能只想读取某个对象(例如购物车)的“当前”值,而不想让该对象发生任何更改而再次触发 effect。针对这种情况,React 引入了“effect 事件”的概念(例如……)。 useEffectEvent),它封装了非响应式代码,可以读取最后一个值而不会成为依赖项。
为了提高性能,通常的做法是将依赖关系数组限制在最小范围内。与其让 effect 没有第二个参数(这样每次渲染后都会触发它),不如传递一个只包含真正需要触发它的值的数组。如果渲染之间没有依赖项发生变化,React 会跳过清理和设置步骤,从而避免重新执行 effect。
useEffect 常见问题排查
最常见的错误之一是导致效果陷入无限循环。这种情况通常发生在你更新 effect 本身的状态时,而该状态又是依赖数组的一部分。effect 改变了状态,状态的改变触发了渲染,渲染又重新执行了 effect,如此循环往复。
要打破这个循环,请回顾一下你为什么要从 effect 中更新该状态。如果你不与外部系统同步,甚至可能不需要使用 useEffect:很多事情都可以在渲染过程中直接处理。如果你要与外部系统同步,请确保状态更新仅依赖于实际的变化,而不是每次都触发。
另一个常见的问题来源是依赖项会随着每次渲染而改变,而你却没有注意到。在 JSX 中创建内联对象或函数并在 effect 中使用它们总是会导致不同的依赖数组,因为每次渲染都会生成一个新的引用。为了避免这种情况,您可以将这些对象的创建移到 effect 本身,或者作为最后的手段,使用 `resolve` 来稳定它们。 useMemo / useCallback.
令人惊讶的是,即使组件仍在屏幕上,清洁功能也会运行。这是正常现象:每次依赖项发生变化时,React 都会在应用新的 effect 之前清理之前的效果。在开发环境中,启用严格模式 (StrictMode) 后,它还会在挂载后立即执行额外的配置和清理操作。
最后,如果你的效果是视觉效果,并且在效果生效前注意到有闪烁,请检查效果是否生效。你可能需要切换到 useLayoutEffect 强制它在浏览器渲染之前运行。这是一个需要谨慎使用的工具,仅在确实需要阻止渲染直到测量或定位完成时才使用。
useEffect 和服务器端渲染 (SSR)
服务器端渲染应用程序的一个重要细节是,特效仅在客户端运行。在服务器端渲染 (SSR) 期间,React 会生成初始 HTML,但不会运行效果。这些效果会在应用加载到浏览器后触发。
这意味着 useEffect 中任何依赖于浏览器 API(例如 localStorage o window) 能够抵御服务器攻击因为它不会直接在那里运行。你需要确保的是,第一次渲染(在服务器端完成,然后在客户端重复执行的渲染)是确定性的,并且不会读取任何仅存在于浏览器中的内容。
如果你想在服务器端和客户端显示不同的内容,一种常见的模式是使用状态,例如: didMount最初它在 false,并且在一个 useEffect 同 [] 你把它穿上 true当该值为假时,您可以渲染组件的“安全”版本;当该值为真时,您就可以从中读取数据了。 localStorage调整 DOM 等。但是,尽量确保视觉变化不会对网络连接速度较慢的用户造成太大影响。
一般来说,组件树与环境的独立性越强,将 SSR 与 Hooks 结合使用时遇到的意外情况就越少。请将 useEffect 保留给与外部系统的实际集成,这才是它的真正用途。
掌握 useState 和 useEffect 需要理解组件在每次渲染时都会“重新创建”,效果属于特定的生命周期,依赖数组描述了代码和响应式数据之间的关系。当你内化了这种思维模型,那种神秘莫测的感觉就会消失,你就能轻松地使用这些 Hooks,编写出更具声明性、更简洁、更易于维护的组件,即使是在大型 React 应用程序中也是如此。
对字节世界和一般技术充满热情的作家。我喜欢通过写作分享我的知识,这就是我在这个博客中要做的,向您展示有关小工具、软件、硬件、技术趋势等的所有最有趣的事情。我的目标是帮助您以简单而有趣的方式畅游数字世界。