副作用
想象一个组件需要在渲染后保持一个定时器继续运行,就像下面的 Timer 组件所做的那样。组件做的大部分工作都发生在渲染时:它读取 props 和 state,然后返回 JSX。让定时器运行是不同的。打开一个订阅、同步文档标题或从服务器获取数据也是这样。每一个都伸向那个世界之外,触及 React 不管理的东西。useEffect 就是为此而生的 hook。它让组件在渲染后运行一些代码,这样它就可以与那个外部的东西同步。
这里是一个只启动一次定时器,然后每秒计数加一的副作用:
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],意味着副作用在第一次渲染后运行,然后在这些值中任何一个在渲染之间改变时再运行。这是你在副作用依赖的某个东西改变时重新同步的方式。 - 完全不提供数组意味着副作用在每次渲染后都运行。这很少是你想要的,也是导致状态更新形成死循环的常见原因。
所以这个数组是你回答"这段代码应该何时再次运行?"这个问题的方式。列出副作用读取并依赖的值,React 会在它们中任何一个改变时重新运行。
清理函数
上面的副作用返回一个函数:
return () => clearInterval(id)这个返回的函数是**清理**函数。React 在副作用再次运行之前运行它,以及在组件从屏幕上移除时再运行一次。它的工作是撤销副作用设置的任何东西。
清理函数很重要,因为副作用经常启动一些会独立继续运行的东西:一个定时器、一个事件监听器、一个打开的连接。如果组件被移除,没有什么停止那个定时器,它会永远继续运行,并保持对已经消失的组件的引用。做几次这样的事,你就会有一个缓慢的泄漏,以及更新降落在不再存在的东西上。经验法则:如果一个副作用启动了什么东西、订阅了什么东西或打开了什么东西,它的清理函数应该停止、取消订阅或关闭它。
你可能不需要副作用
副作用是为了与外部世界同步的,所以很多看起来像 useEffect 的候选代码实际上应该放在别的地方。有两种情况经常出现。
第一种是衍生值。如果你能从已有的 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 之外的地方拉入数据。

