副作用
想象一个组件需要在渲染后保持计时器运行,就像下面的 Timer 组件一样。组件的大部分工作都发生在渲染阶段:它读取 props 和 state,然后返回 JSX。但保持计时器运行是另一回事。打开一个订阅、同步文档标题,或者从服务器获取数据也都是这样。这些操作都会伸出手去接触 React 不管理的东西。useEffect 就是为此而生的钩子。它让组件在渲染后运行一些代码,这样它就可以与外部的东西保持同步。
这是一个启动计时器一次、然后每秒让计数器增加一的副作用:
import { useState, useEffect } from 'react'
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000)
return () => clearInterval(id)
}, [])
return <p>{count}</p>
}这里有三个部分在工作,它们对应每个副作用要处理的三件事:做什么、如何清理,以及何时运行。
传给 useEffect 的函数就是副作用本身。这个例子调用 setInterval 来启动一个重复的计时器,每秒让 count 加一。
依赖数组
第二个参数,最后的 [],就是**依赖数组**。它控制副作用何时运行。
- 空数组
[]表示副作用在组件首次出现在屏幕上后运行一次。上面的计时器就用的这个:启动计时器一次,然后让它一直运行。 - 数组中有值,比如
[a, b],表示副作用在首次渲染后运行,以及任何时候这些值在两次渲染之间发生变化时再运行。这就是当副作用依赖的东西改变时重新同步的方式。 - 完全不写数组,表示副作用在每次渲染后都运行。这很少是你想要的,当副作用也会更新 state 时,这是引发无限循环的常见原因。
所以这个数组回答的问题是"这段代码应该何时再运行一次?"列出副作用读取和依赖的值,React 就会在任何一个值改变时重新运行它。
清理函数
上面的副作用返回了一个函数:
return () => clearInterval(id)这个返回的函数就是**清理**函数。React 在副作用再次运行之前会执行它,以及在组件从屏幕上移除时再执行一次。它的工作是撤销副作用所做的设置。
清理很重要,因为副作用经常会启动一些自己继续运行的东西:一个计时器、一个事件监听器、一个打开的连接。如果组件被移除了而什么都没有停止那个计时器,它会永远继续触发,并持有对一个已经消失的组件的引用。干这几次,你就会有一个缓慢的泄漏,状态更新发送到已经不存在的东西。经验法则:如果副作用启动了东西、订阅了东西,或者打开了东西,它的清理函数应该停止、取消订阅,或者关闭它。
你可能根本不需要副作用
副作用是用来与外部世界同步的,所以很多看起来像是副作用的候选代码其实应该放在别的地方。有两种情况经常出现。
第一种是衍生值。如果你可以从已有的 props 和 state 计算出什么东西,就在渲染的时候直接计算它:
function Cart({ items }) {
const total = items.reduce((sum, item) => sum + item.price, 0)
return <p>Total: {total}</p>
}total 是从 items 衍生出来的,所以它在每次渲染时都会被计算,永远保持同步。把它存在自己的 state 里然后用副作用更新它,只会增加代码量和多出一次渲染,得不到任何好处。
第二种是响应用户操作。当某些事应该发生是因为用户点击或输入了什么,那段代码应该放在那个操作的事件处理函数里:
function BuyButton({ product }) {
function handleClick() {
buyProduct(product)
}
return <button onClick={handleClick}>Buy</button>
}购买就在 handleClick 里发生,正好在点击被处理的地方。通过副作用来路由它只会增加一层间接性,让流程更难跟踪。一个好的测试:如果代码因为一个特定的交互而运行,用事件处理函数;如果它运行是为了让组件与一个外部系统保持同步,用副作用。
版本说明
在类组件中这个行为被分散在三个生命周期方法里:componentDidMount 用于首次渲染后的设置,componentDidUpdate 用于数据改变时的重新运行,componentWillUnmount 用于清理。一个带依赖数组和清理函数的 useEffect 覆盖了这全部三个,这就是为什么相关的设置和清理代码现在住在一起而不是分散在独立的方法里。
useEffect 看作是代码伸出手去接触 React 之外东西的地方:一个计时器、文档标题、一个服务器请求。 最后的数组说的是何时运行它,[] 表示一次,就在组件出现后。如果你的副作用启动了什么东西,返回一个小函数来停止它。
很多时候你根本不需要副作用,所以养成习惯在写副作用前停下来问问一个简单的计算或点击处理函数是否能做这个工作。
接下来:获取数据,这是组件从完全在 React 之外的地方拉入数据的地方。

