Hook 简介
Hook 概览
使用 State Hook
使用 Effect Hook
Hook 规则
自定义 Hook
Hook API 索引
Hooks FAQ
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量 const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
useState
是我们要学习的第一个 “Hook”,这个例子是简单演示。如果不理解也不用担心。
你将在下一章节正式开始学习 Hook。 这一章节,我们将会解释为什么会在 React 中加入 Hook,以及如何使用 Hook 写出更好的应用。
注意
React 16.8.0 是第一个支持 Hook 的版本。升级时,请注意更新所有的 package,包括 React DOM。 React Native 从 0.59 版本开始支持 Hook。
在 React Conf 2018 上,Sophie Alpert 和 Dan Abramov 介绍了 Hook,紧接着 Ryan Florence 演示了如何使用 Hook 重构应用。你可以在这里看到这个视频:
在我们继续之前,请记住 Hook 是:
没有计划从 React 中移除 class。 你可以在本页底部的章节读到更多关于 Hook 的渐进策略。
Hook 不会影响你对 React 概念的理解。 恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后我们将看到,Hook 还提供了一种更强大的方式来组合他们。
如果不想了解添加 Hook 的具体原因,可以直接跳到下一章节开始学习 Hook! 当然你也可以继续阅读这一章节来了解原因,并且可以学习到如何在不重写应用的情况下使用 Hook。
Hook 解决了我们五年来编写和维护成千上万的组件时遇到的各种各样看起来不相关的问题。无论你正在学习 React,或每天使用,或者更愿尝试另一个和 React 有相似组件模型的框架,你都可能对这些问题似曾相识。
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。
你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。
具体将在自定义 Hook 中对此展开更多讨论。
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount
和 componentDidUpdate
中获取数据。但是,同一个 componentDidMount
中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount
中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。
为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
我们将在使用 Effect Hook 中对此展开更多讨论。
除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this
的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。
另外,React 已经发布五年了,我们希望它能在下一个五年也与时俱进。就像 Svelte,Angular,Glimmer等其它的库展示的那样,组件预编译会带来巨大的潜力。尤其是在它不局限于模板的时候。最近,我们一直在使用 Prepack 来试验 component folding,也取得了初步成效。但是我们发现使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。
为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
示例
Hook 概览是开始学习 Hook 的不错选择。
总结:没有计划从 React 中移除 class。
大部分 React 开发者会专注于开发产品,而没时间关注每一个新 API 的发布。Hook 还很新,也许等到有更多示例和教程后,再考虑学习或使用它们也不迟。
我们也明白向 React 添加新的原生概念的门槛非常高。我们为好奇的读者准备了详细的征求意见文档,在文档中用更多细节深入讨论了我们推进这件事的动机,也在具体设计决策和相关先进技术上提供了额外的视角。
最重要的是,Hook 和现有代码可以同时工作,你可以渐进式地使用他们。 不用急着迁移到 Hook。我们建议避免任何“大规模重写”,尤其是对于现有的、复杂的 class 组件。开始“用 Hook 的方式思考”前,需要做一些思维上的转变。按照我们的经验,最好先在新的不复杂的组件中尝试使用 Hook,并确保团队中的每一位成员都能适应。在你尝试使用 Hook 后,欢迎给我们提供反馈,无论好坏。
我们准备让 Hook 覆盖所有 class 组件的使用场景,但是我们将继续为 class 组件提供支持。在 Facebook,我们有成千上万的组件用 class 书写,我们完全没有重写它们的计划。相反,我们开始在新的代码中同时使用 Hook 和 class。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hook 是向下兼容的。本页面为有经验的 React 用户提供一个对 Hook 的概览。这是一个相当快速的概览,如果你有疑惑,可以参阅下面这样的黄色提示框。
详细说明
有关我们为什么要在 React 中引入 Hook 的原因,请参考动机。
↑↑↑ 每个部分的结尾都会有一个如上所示的黄色方框。 它们会链接到更详细的说明。
这个例子用来显示一个计数器。当你点击按钮,计数器的值就会增加:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量。 const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
在这里,useState
就是一个 Hook (等下我们会讲到这是什么意思)。通过在函数组件里调用它来给组件添加一些内部 state。React 会在重复渲染时保留这个 state。useState
会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState
,但是它不会把新的 state 和旧的 state 进行合并。(我们会在使用 State Hook 里展示一个对比 useState
和 this.state
的例子)。
useState
唯一的参数就是初始 state。在上面的例子中,我们的计数器是从零开始的,所以初始 state 就是 0
。值得注意的是,不同于 this.state
,这里的 state 不一定要是一个对象 —— 如果你有需要,它也可以是。这个初始 state 参数只有在第一次渲染时会被用到。
声明多个 state 变量
你可以在一个组件中多次使用 State Hook:
function ExampleWithManyStates() {
// 声明多个 state 变量!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}
数组解构的语法让我们在调用 useState
时可以给 state 变量取不同的名字。当然,这些名字并不是 useState
API 的一部分。React 假设当你多次调用 useState
的时候,你能保证每次渲染时它们的调用顺序是不变的。后面我们会再次解释它是如何工作的以及在什么场景下使用。
那么,什么是 Hook?
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。(我们不推荐把你已有的组件全部重写,但是你可以在新组件里开始使用 Hook。)
React 内置了一些像 useState
这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。我们会先介绍这些内置的 Hook。
详细说明
你可以在这一章节了解更多关于 State Hook 的内容:使用 State Hook。
你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。(我们会在使用 Effect Hook 里展示对比 useEffect
和这些方法的例子。)
例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate: useEffect(() => { // 使用浏览器的 API 更新页面标题 document.title = `You clicked ${count} times`; });
return (
You clicked {count} times
);
}
当你调用 useEffect
时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。(我们会在使用 Effect Hook 中跟 class 组件的生命周期方法做更详细的对比。)
副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
在这个示例中,React 会在组件销毁时取消对 ChatAPI
的订阅,然后在后续渲染时重新执行副作用函数。(如果传给 ChatAPI
的 props.friend.id
没有变化,你也可以告诉 React 跳过重新订阅。)
跟 useState
一样,你可以在组件中多次使用 useEffect
:
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...
通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。
详细说明
你可以在这一章节了解更多关于
useEffect
的内容:使用 Effect Hook
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
同时,我们提供了 linter 插件来自动执行这些规则。这些规则乍看起来会有一些限制和令人困惑,但是要让 Hook 正常工作,它们至关重要。
详细说明
你可以在这章节了解更多关于这些规则的内容:Hook 使用规则。
有时候我们会想要在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:高阶组件和 render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。
前面,我们介绍了一个叫 FriendStatus
的组件,它通过调用 useState
和 useEffect
的 Hook 来订阅一个好友的在线状态。假设我们想在另一个组件里重用这个订阅逻辑。
首先,我们把这个逻辑抽取到一个叫做 useFriendStatus
的自定义 Hook 里:
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
它将 friendID
作为参数,并返回该好友是否在线:
现在我们可以在两个组件中使用它:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
{props.friend.name}
);
}
这两个组件的 state 是完全独立的。Hook 是一种复用状态逻辑的方式,它不复用 state 本身。事实上 Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。
自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use
” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 useSomething
的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。
你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有更多我们没想到的场景。我们很期待看到 React 社区会出现什么样的自定义 Hook。
详细说明
我们会在这一章节介绍更多关于自定义 Hook 的内容: 创建你自己的 Hook。
除此之外,还有一些使用频率较低的但是很有用的 Hook。比如,useContext
让你不使用组件嵌套就可以订阅 React 的 Context。
function Example() {
const locale = useContext(LocaleContext); const theme = useContext(ThemeContext); // ...
}
另外 useReducer
可以让你通过 reducer 来管理组件本地的复杂 state。
function Todos() {
const [todos, dispatch] = useReducer(todosReducer); // ...
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hook 简介章节中使用下面的例子介绍了 Hook:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量 const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
我们将通过将这段代码与一个等价的 class 示例进行比较来开始学习 Hook。
如果你之前在 React 中使用过 class,这段代码看起来应该很熟悉:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
You clicked {this.state.count} times
);
}
}
state 初始值为 { count: 0 }
,当用户点击按钮后,我们通过调用 this.setState()
来增加 state.count
。整个章节中都将使用该 class 的代码片段做示例。
注意
你可能想知道为什么我们在这里使用一个计数器例子而不一个更实际的示例。因为我们还只是初步接触 Hook ,这可以帮助我们将注意力集中到 API 本身。
复习一下, React 的函数组件是这样的:
const Example = (props) => {
// 你可以在这使用 Hook
return ;
}
或是这样:
function Example(props) {
// 你可以在这使用 Hook
return ;
}
你之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。
Hook 在 class 内部是不起作用的。但你可以使用它们来取代 class 。
在新示例中,首先引入 React 中 useState
的 Hook
import React, { useState } from 'react';
function Example() {
// ...
}
Hook 是什么? Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState
是允许你在 React 函数组件中添加 state 的 Hook。稍后我们将学习其他 Hook。
什么时候我会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
注意:
在组件中有些特殊的规则,规定什么地方能使用 Hook,什么地方不能使用。我们将在 Hook 规则中学习它们。
在 class 中,我们通过在构造函数中设置 this.state
为 { count: 0 }
来初始化 count
state 为 0
:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 }; }
在函数组件中,我们没有 this
,所以我们不能分配或读取 this.state
。我们直接在组件中调用 useState
Hook:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量 const [count, setCount] = useState(0);
调用 useState
方法的时候做了什么? 它定义一个 “state 变量”。我们的变量叫 count
, 但是我们可以叫他任何名字,比如 banana
。这是一种在函数调用时保存变量的方式 —— useState
是一种新方法,它与 class 里面的 this.state
提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
useState
需要哪些参数? useState()
方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0
作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState()
两次即可。)
useState
方法的返回值是什么? 返回值为:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState()
的原因。这与 class 里面 this.state.count
和 this.setState
类似,唯一区别就是你需要成对的获取它们。如果你不熟悉我们使用的语法,我们会在本章节的底部介绍它。
既然我们知道了 useState
的作用,我们的示例应该更容易理解了:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量 const [count, setCount] = useState(0);
我们声明了一个叫 count
的 state 变量,然后把它设为 0
。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount
来更新当前的 count
。
注意
你可能想知道:为什么叫
useState
而不叫createState
?“Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,
useState
返回给我们当前的 state。否则它就不是 “state”了!这也是 Hook 的名字总是以use
开头的一个原因。我们将在后面的 Hook 规则中了解原因。
当我们想在 class 中显示当前的 count,我们读取 this.state.count
:
You clicked {this.state.count} times
在函数中,我们可以直接用 count
:
You clicked {count} times
在 class 中,我们需要调用 this.setState()
来更新 count
值:
在函数中,我们已经有了 setCount
和 count
变量,所以我们不需要 this
:
现在让我们来仔细回顾一下学到的知识,看下我们是否真正理解了。
1: import React, { useState } from 'react'; 2:
3: function Example() {
4: const [count, setCount] = useState(0); 5:
6: return (
7:
8: You clicked {count} times
9:
12:
13: );
14: }
useState
Hook。它让我们在函数组件中存储内部 state。Example
组件内部,我们通过调用 useState
Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count
,因为它存储的是点击次数。我们通过传 0
作为 useState
唯一的参数来将其初始化为 0
。第二个返回的值本身就是一个函数。它让我们可以更新 count
的值,所以我们叫它 setCount
。setCount
。React 会重新渲染 Example
组件,并把最新的 count
传给它。乍一看这似乎有点太多了。不要急于求成!如果你有不理解的地方,请再次查看以上代码并从头到尾阅读。我们保证一旦你试着”忘记” class 里面 state 是如何工作的,并用新的眼光看这段代码,就容易理解了。
你可能注意到我们用方括号定义了一个 state 变量
const [count, setCount] = useState(0);
等号左边名字并不是 React API 的部分,你可以自己取名字:
const [fruit, setFruit] = useState('banana');
这种 JavaScript 语法叫数组解构。它意味着我们同时创建了 fruit
和 setFruit
两个变量,fruit
的值为 useState
返回的第一个值,setFruit
是返回的第二个值。它等价于下面的代码:
var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组
var fruit = fruitStateVariable[0]; // 数组里的第一个值
var setFruit = fruitStateVariable[1]; // 数组里的第二个值
当我们使用 useState
定义 state 变量时候,它返回一个有两个值的数组。第一个值是当前的 state,第二个值是更新 state 的函数。使用 [0]
和 [1]
来访问有点令人困惑,因为它们有特定的含义。这就是我们使用数组解构的原因。
注意
你可能会好奇 React 怎么知道
useState
对应的是哪个组件,因为我们并没有传递this
给 React。我们将在 FAQ 部分回答这个问题以及许多其他问题。
将 state 变量声明为一对 [something, setSomething]
也很方便,因为如果我们想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称:
function ExampleWithManyStates() {
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
在以上组件中,我们有局部变量 age
,fruit
和 todos
,并且我们可以单独更新它们:
function handleOrangeClick() {
// 和 this.setState({ fruit: 'orange' }) 类似
setFruit('orange');
}
你不必使用多个 state 变量。State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组。然而,不像 class 中的 this.setState
,更新 state 变量总是替换它而不是合并它。
我们在 FAQ 中提供了更多关于分离独立 state 变量的建议。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Effect Hook 可以让你在函数组件中执行副作用操作
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; });
return (
You clicked {count} times
);
}
这段代码基于上一章节中的计数器示例进行修改,我们为计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。
提示
如果你熟悉 React class 的生命周期函数,你可以把
useEffect
Hook 看做componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个函数的组合。
在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。我们来更仔细地看一下他们之间的区别。
有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。
在 React 的 class 组件中,render
函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。
这就是为什么在 React class 中,我们把副作用操作放到 componentDidMount
和 componentDidUpdate
函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; }
render() {
return (
You clicked {this.state.count} times
);
}
}
注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。
这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
现在让我们来看看如何使用 useEffect
执行相同的操作。
我们在本章节开始时已经看到了这个示例,但让我们再仔细观察它:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `You clicked ${count} times`; });
return (
You clicked {count} times
);
}
useEffect
做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用 useEffect
? 将 useEffect
放在组件内部让我们可以在 effect 中直接访问 count
state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect
会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
现在我们已经对 effect 有了大致了解,下面这些代码应该不难看懂了:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
我们声明了 count
state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect
Hook。此函数就是我们的 effect。然后使用 document.title
浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count
值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。
经验丰富的 JavaScript 开发人员可能会注意到,传递给 useEffect
的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count
的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。我们将在本章节后续部分更清楚地了解这样做的意义。
提示
与
componentDidMount
或componentDidUpdate
不同,使用useEffect
调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffect
Hook 供你使用,其 API 与useEffect
相同。
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。
在 React class 中,你通常会在 componentDidMount
中设置订阅,并在 componentWillUnmount
中清除它。例如,假设我们有一个 ChatAPI
模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); }
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
你会注意到 componentDidMount
和 componentWillUnmount
之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。
注意
眼尖的读者可能已经注意到了,这个示例还需要编写
componentDidUpdate
方法才能保证完全正确。我们先暂时忽略这一点,本章节中后续部分会介绍它。
如何使用 Hook 编写这个组件。
你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect
的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // Specify how to clean up after this effect: return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。我们稍后将讨论为什么这将助于避免 bug以及如何在遇到性能问题时跳过此行为。
注意
并不是必须为 effect 中返回的函数命名。这里我们将其命名为
cleanup
是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。
了解了 useEffect
可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
其他的 effect 可能不必清除,所以不需要返回。
useEffect(() => {
document.title = `You clicked ${count} times`;
});
effect Hook 使用同一个 API 来满足这两种情况。
如果你对 Effect Hook 的机制已经有很好的把握,或者暂时难以消化更多内容,你现在就可以跳转到下一章节学习 Hook 的规则。
在本节中将继续深入了解 useEffect
的某些特性,有经验的 React 使用者可能会对此感兴趣。你不一定要在现在了解他们,你可以随时查看此页面以了解有关 Effect Hook 的更多详细信息。
使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。下述代码是将前述示例中的计数器和好友在线状态指示器逻辑组合在一起的组件:
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
可以发现设置 document.title
的逻辑是如何被分割到 componentDidMount
和 componentDidUpdate
中的,订阅逻辑又是如何被分割到 componentDidMount
和 componentWillUnmount
中的。而且 componentDidMount
中同时包含了两个不同功能的代码。
那么 Hook 如何解决这个问题呢?就像你可以使用多个 state 的 Hook 一样,你也可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中:
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
如果你已经习惯了使用 class,那么你或许会疑惑为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。让我们看一个实际的例子,看看为什么这个设计可以帮助我们创建 bug 更少的组件。
在本章节开始时,我们介绍了一个用于显示好友是否在线的 FriendStatus
组件。从 class 中 props 读取 friend.id
,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
但是当组件已经显示在屏幕上时,friend
prop 发生变化时会发生什么? 我们的组件将继续展示原来的好友状态。这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。
在 class 组件中,我们需要添加 componentDidUpdate
来解决这个问题:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) { // 取消订阅之前的 friend.id ChatAPI.unsubscribeFromFriendStatus( prevProps.friend.id, this.handleStatusChange ); // 订阅新的 friend.id ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
忘记正确地处理 componentDidUpdate
是 React 应用中常见的 bug 来源。
现在看一下使用 Hook 的版本:
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
它并不会受到此 bug 影响。(虽然我们没有对它做任何改动。)
并不需要特定的代码来处理更新逻辑,因为 useEffect
默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 运行第一个 effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 运行下一个 effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 运行下一个 effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect
此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate
中添加对 prevProps
或 prevState
的比较逻辑解决:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
这是很常见的需求,所以它被内置到了 useEffect
的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect
的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
上面这个示例中,我们传入 [count]
作为第二个参数。这个参数是什么作用呢?如果 count
的值是 5
,而且我们的组件重渲染的时候 count
还是等于 5
,React 将对前一次渲染的 [5]
和后一次渲染的 [5]
进行比较。因为数组中的所有元素都是相等的(5 === 5
),React 会跳过这个 effect,这就实现了性能的优化。
当渲染时,如果 count
的值更新成了 6
,React 将会把前一次渲染时的数组 [5]
和这次渲染的数组 [6]
中的元素进行对比。这次因为 5 !== 6
,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
对于有清除操作的 effect 同样适用:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
未来版本,可能会在构建时自动添加第二个参数。
注意:
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。参阅文档,了解更多关于如何处理函数以及数组频繁变化时的措施内容。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(
[]
)作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。如果你传入了一个空数组(
[]
),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入[]
作为第二个参数更接近大家更熟悉的componentDidMount
和componentWillUnmount
思维模式,但我们有更好的方式来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用useEffect
,因此会使得额外操作很方便。我们推荐启用
eslint-plugin-react-hooks
中的exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则。我们提供了一个 linter 插件来强制执行这些规则:
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState
和 useEffect
调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在下面会有更深入的解释。)
不要在普通的 JavaScript 函数中调用 Hook。你可以:
遵循此规则,确保组件的状态逻辑在代码中清晰可见。
我们发布了一个名为 eslint-plugin-react-hooks
的 ESLint 插件来强制执行这两条规则。如果你想尝试一下,可以将此插件添加到你的项目中:
我们打算后续版本默认添加此插件到 Create React App 及其他类似的工具包中。
npm install eslint-plugin-react-hooks --save-dev
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}
现在你可以跳转到下一章节学习如何编写你自己的 Hook。在本章节中,我们将继续解释这些规则背后的原因。
正如我们之前学到的,我们可以在单个组件中使用多个 State Hook 或 Effect Hook
function Form() {
// 1. Use the name state variable
const [name, setName] = useState('Mary');
// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
// 3. Use the surname state variable
const [surname, setSurname] = useState('Poppins');
// 4. Use an effect for updating the title
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});
// ...
}
那么 React 怎么知道哪个 state 对应哪个 useState
?答案是 React 靠的是 Hook 调用的顺序。因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:
// ------------
// 首次渲染
// ------------
useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// -------------
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm) // 2. 替换保存 form 的 effect
useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect
// ...
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook (例如 persistForm
effect) 调用放到一个条件语句中会发生什么呢?
// 在条件语句中使用 Hook 违反第一条规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
在第一次渲染中 name !== ''
这个条件值为 true
,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false
。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm) // 此 Hook 被忽略!
useState('Poppins') // 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle) // 3 (之前为 4)。替换更新标题的 effect 失败
React 不知道第二个 useState
的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应得是 persistForm
的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。
这就是为什么 Hook 需要在我们组件的最顶层调用。如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:
useEffect(function persistForm() {
// 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});
注意:如果使用了提供的 lint 插件,就无需担心此问题。 不过你现在知道了为什么 Hook 会这样工作,也知道了这个规则是为了避免什么问题。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
在我们学习使用 Effect Hook 时,我们已经见过这个聊天程序中的组件,该组件用于显示好友的在线状态:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
现在我们假设聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色。我们可以把上面类似的逻辑复制并粘贴到 FriendListItem
组件中来,但这并不是理想的解决方案:
import React, { useState, useEffect } from 'react';
function FriendListItem(props) {
const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
return (
{props.friend.name}
);
}
相反,我们希望在 FriendStatus
和 FriendListItem
之间共享逻辑。
目前为止,在 React 中有两种流行的方式来共享组件之间的状态逻辑: render props 和高阶组件,现在让我们来看看 Hook 是如何在让你不增加组件的情况下解决相同问题的。
当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。
自定义 Hook 是一个函数,其名称以 “use
” 开头,函数内部可以调用其他的 Hook。 例如,下面的 useFriendStatus
是我们第一个自定义的 Hook:
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
此处并未包含任何新的内容——逻辑是从上述组件拷贝来的。与组件中一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。
与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use
开头,这样可以一眼看出其符合 Hook 的规则。
此处 useFriendStatus
的 Hook 目的是订阅某个好友的在线状态。这就是我们需要将 friendID
作为参数,并返回这位好友的在线状态的原因。
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
return isOnline;
}
现在让我们看看应该如何使用自定义 Hook。
我们一开始的目标是在 FriendStatus
和 FriendListItem
组件中去除重复的逻辑,即:这两个组件都想知道好友是否在线。
现在我们已经把这个逻辑提取到 useFriendStatus
的自定义 Hook 中,然后就可以使用它了:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
{props.friend.name}
);
}
这段代码等价于原来的示例代码吗?等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。
自定义 Hook 必须以 “use
” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
自定义 Hook 如何获取独立的 state?每次调用 Hook,它都会获取独立的 state。由于我们直接调用了 useFriendStatus
,从 React 的角度来看,我们的组件只是调用了 useState
和 useEffect
。 正如我们在之前章节中了解到的一样,我们可以在一个组件中多次调用 useState
和 useEffect
,它们是完全独立的。
由于 Hook 本身就是函数,因此我们可以在它们之间传递信息。
我们将使用聊天程序中的另一个组件来说明这一点。这是一个聊天消息接收者的选择器,它会显示当前选定的好友是否在线:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
>
);
}
我们将当前选择的好友 ID 保存在 recipientID
状态变量中,并在用户从 中选择其他好友时更新这个 state。
由于 useState
为我们提供了 recipientID
状态变量的最新值,因此我们可以将它作为参数传递给自定义的 useFriendStatus
Hook:
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
如此可以让我们知道当前选中的好友是否在线。当我们选择不同的好友并更新 recipientID
状态变量时,useFriendStatus
Hook 将会取消订阅之前选中的好友,并订阅新选中的好友状态。
useYourImagination()
自定义 Hook 解决了以前在 React 组件中无法灵活共享逻辑的问题。你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有其他我们没想到的场景。更重要的是,创建自定义 Hook 就像使用 React 内置的功能一样简单。
尽量避免过早地增加抽象逻辑。既然函数组件能够做的更多,那么代码库中函数组件的代码行数可能会剧增。这属于正常现象 —— 不必立即将它们拆分为 Hook。但我们仍鼓励你能通过自定义 Hook 寻找可能,以达到简化代码逻辑,解决组件杂乱无章的目的。
例如,有个复杂的组件,其中包含了大量以特殊的方式来管理的内部状态。useState
并不会使得集中更新逻辑变得容易,因此你可能更愿意使用 redux 中的 reducer 来编写。
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, {
text: action.text,
completed: false
}];
// ... other actions ...
default:
return state;
}
}
Reducers 非常便于单独测试,且易于扩展,以表达复杂的更新逻辑。如有必要,您可以将它们分成更小的 reducer。但是,你可能还享受着 React 内部 state 带来的好处,或者可能根本不想安装其他库。
那么,为什么我们不编写一个 useReducer
的 Hook,使用 reducer 的方式来管理组件的内部 state 呢?其简化版本可能如下所示:
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
在组件中使用它,让 reducer 驱动它管理 state:
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: 'add', text });
}
// ...
}
在复杂组件中使用 reducer 管理内部 state 的需求很常见,我们已经将 useReducer
的 Hook 内置到 React 中。你可以在 Hook API 索引中找到它使用,搭配其他内置的 Hook 一起使用。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
本页面主要描述 React 中内置的 Hook API。
如果你刚开始接触 Hook,那么可能需要先查阅 Hook 概览。你也可以在 Hooks FAQ 章节中获取有用的信息。
基础 Hook
useState
useEffect
useContext
额外的 Hook
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
useState
const [state, setState] = useState(initialState);
返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state
) 与传入的第一个参数 (initialState
) 值相同。
setState
函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
setState(newState);
在后续的重新渲染中,useState
返回的第一个值将始终是更新后最新的 state。
注意
React 会确保
setState
函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从useEffect
或useCallback
的依赖列表中省略setState
。
函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState
。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState
的两种用法:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
>
);
}
“+” 和 “-” 按钮采用函数式形式,因为被更新的 state 需要基于之前的 state。但是“重置”按钮则采用普通形式,因为它总是把 count 设置回初始值。
如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
注意
与 class 组件中的
setState
方法不同,useState
不会自动合并更新对象。你可以用函数式的setState
结合展开运算符来达到合并更新对象的效果。setState(prevState => { // 也可以使用 Object.assign return {...prevState, ...updatedValues}; });
useReducer
是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。
惰性初始 state
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
跳过 state 更新
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is
比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo
来进行优化。
useEffect
useEffect(didUpdate);
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect
完成副作用操作。赋值给 useEffect
的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。
清除 effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect
函数需返回一个清除函数。以下就是一个创建订阅的例子:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。在上述示例中,意味着组件的每一次更新都会创建新的订阅。若想避免每次更新都触发 effect 的执行,请参阅下一小节。
effect 的执行时机
与 componentDidMount
、componentDidUpdate
不同的是,在浏览器完成布局与绘制之后,传给 useEffect
的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect
Hook 来处理这类 effect。它和 useEffect
的结构相同,区别只是调用时机不同。
虽然 useEffect
会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。
effect 的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source
prop 改变时重新创建。
要实现这一点,可以给 useEffect
传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
此时,只有当 props.source
改变后才会重新创建订阅。
注意
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。请参阅文档,了解更多关于如何处理函数 以及数组频繁变化时的措施 的内容。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(
[]
)作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。如果你传入了一个空数组(
[]
),effect 内部的 props 和 state 就会一直持有其初始值。尽管传入[]
作为第二个参数有点类似于componentDidMount
和componentWillUnmount
的思维模式,但我们有 更好的 方式 来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用useEffect
,因此会使得处理额外操作很方便。我们推荐启用
eslint-plugin-react-hooks
中的exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
依赖项数组不会作为参数传给 effect 函数。虽然从概念上来说它表现为:所有 effect 函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
useContext
const value = useContext(MyContext);
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的
的 value
prop 决定。
当组件上层最近的
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。即使祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件本身使用 useContext
时重新渲染。
别忘记 useContext
的参数必须是 context 对象本身:
useContext(MyContext)
useContext(MyContext.Consumer)
useContext(MyContext.Provider)
调用了 useContext
的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。
提示
如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,
useContext(MyContext)
相当于 class 组件中的static contextType = MyContext
或者。
useContext(MyContext)
只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用来为下层组件提供 context。
把如下代码与 Context.Provider 放在一起
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
);
}
function Toolbar(props) {
return (
);
}
function ThemedButton() {
const theme = useContext(ThemeContext); return ( );
}
对先前 Context 高级指南中的示例使用 hook 进行了修改,你可以在链接中找到有关如何 Context 的更多信息。
以下介绍的 Hook,有些是上一节中基础 Hook 的变体,有些则仅在特殊情况下会用到。不用特意预先学习它们。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch
而不是回调函数 。
以下是用 reducer 重写 useState
一节的计数器示例:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
}
注意
React 会确保
dispatch
函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从useEffect
或useCallback
的依赖列表中省略dispatch
。
指定初始 state
有两种不同初始化 useReducer
state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer
是最简单的方法:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount} );
注意
React 不使用
state = initialState
这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因此需要在调用 Hook 时指定。如果你特别喜欢上述的参数约定,可以通过调用useReducer(reducer, undefined, reducer)
来模拟 Redux 的行为,但我们不鼓励你这么做。
惰性初始化
你可以选择惰性地创建初始 state。为此,需要将 init
函数作为 useReducer
的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
。
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
function init(initialCount) { return {count: initialCount};}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset': return init(action.payload); default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init); return (
<>
Count: {state.count}
>
);
}
跳过 dispatch
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is
比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo
来进行优化。
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
注意
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
我们推荐启用
eslint-plugin-react-hooks
中的exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
你可以把 useMemo
作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
注意
依赖项数组不会作为参数传给“创建”函数。虽然从概念上来说它表现为:所有“创建”函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
我们推荐启用
eslint-plugin-react-hooks
中的exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
useRef
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
一个常见的用例便是命令式地访问子组件:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
>
);
}
本质上,useRef
就像是可以在其 .current
属性中保存一个可变值的“盒子”。
你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的
.current
属性设置为相应的 DOM 节点。
然而,useRef()
比 ref
属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
这是因为它创建的是一个普通 Javascript 对象。而 useRef()
和自建一个 {current: ...}
对象的唯一区别是,useRef
会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与 forwardRef
一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return ;
}
FancyInput = forwardRef(FancyInput);
在本例中,渲染
的父组件可以调用 inputRef.current.focus()
。
useLayoutEffect
其函数签名与 useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect
以避免阻塞视觉更新。
提示
如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意
useLayoutEffect
与componentDidMount
、componentDidUpdate
的调用阶段是一样的。但是,我们推荐你一开始先用useEffect
,只有当它出问题的时候再尝试使用useLayoutEffect
。如果你使用服务端渲染,请记住,无论
useLayoutEffect
还是useEffect
都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入useLayoutEffect
代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至useEffect
中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到useLayoutEffect
执行之前 HTML 都显示错乱的情况下)。若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用
showChild &&
进行条件渲染,并使用useEffect(() => { setShowChild(true); }, [])
延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
useDebugValue
useDebugValue(value)
useDebugValue
可用于在 React 开发者工具中显示自定义 hook 的标签。
例如,“自定义 Hook” 章节中描述的名为 useFriendStatus
的自定义 Hook:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签 // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
提示
我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
延迟格式化 debug 值
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。
因此,useDebugValue
接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
例如,一个返回 Date
值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString
函数调用:
useDebugValue(date, date => date.toDateString());
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
此章节回答了关于 Hook 的常见问题。
采纳策略
从 Class 迁移到 Hook
性能优化
底层原理
从 16.8.0 开始,React 在以下模块中包含了 React Hook 的稳定实现:
请注意,要启用 Hook,所有 React 相关的 package 都必须升级到 16.8.0 或更高版本。如果你忘记更新诸如 React DOM 之类的 package,Hook 将无法运行。
React Native 0.59 及以上版本支持 Hook。
不。我们并 没有计划 从 React 中移除 class —— 我们也需要不断地发布产品,重写成本较高。我们推荐在新代码中尝试 Hook。
Hook 提供了强大而富有表现力的方式来在组件间复用功能。通过 「自定义 Hook」 这一节可以了解能用它做些什么。这篇来自一位 React 核心团队的成员的 文章 则更加深入地剖析了 Hook 解锁了哪些新的能力。
Hook 是使用你已经知道的 React 特性的一种更直接的方式 —— 比如 state,生命周期,context,以及 refs。它们并没有从根本上改变 React 的工作方式,你对组件,props, 以及自顶向下的数据流的知识并没有改变。
Hook 确实有它们自己的学习曲线。如果这份文档中遗失了一些什么,提一个 issue,我们会尽可能地帮你。
当你准备好了,我们鼓励你在写新组件的时候开始尝试 Hook。请确保你团队中的每个人都愿意使用它们并且熟知这份文档中的内容。我们不推荐用 Hook 重写你已有的 class,除非你本就打算重写它们。(例如:为了修复bug)。
你不能在 class 组件内部使用 Hook,但毫无疑问你可以在组件树里混合使用 class 组件和使用了 Hook 的函数组件。不论一个组件是 class 还是一个使用了 Hook 的函数,都只是这个组件的实现细节而已。长远来看,我们期望 Hook 能够成为人们编写 React 组件的主要方式。
我们给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate
,getDerivedStateFromError
和 componentDidCatch
生命周期的 Hook 等价写法,但我们计划尽早把它们加进来。
目前 Hook 还处于早期阶段,一些第三方的库可能还暂时无法兼容 Hook。
通常,render props 和高阶组件只渲染一个子节点。我们认为让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderItem
属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。
connect()
和 React Router 等流行的 API 来说,意味着什么?你可以继续使用之前使用的 API;它们仍会继续有效。
React Redux 从 v7.1.0 开始支持 Hook API 并暴露了 useDispatch
和 useSelector
等 hook。
React Router 从 v5.1 开始支持 hook。
其它第三库也将即将支持 hook。
Hook 在设计阶段就考虑了静态类型的问题。因为它们是函数,所以它们比像高阶组件这样的模式更易于设定正确的类型。最新版的 Flow 和 TypeScript React 定义已经包含了对 React Hook 的支持。
重要的是,在你需要严格限制类型的时候,自定义 Hook 能够帮你限制 React 的 API。React 只是给你提供了基础功能,具体怎么用就是你自己的事了。
在 React 看来,一个使用了 Hook 的组件只不过是一个常规组件。如果你的测试方案不依赖于 React 的内部实现,测试带 Hook 的组件应该和你通常测试组件的方式没什么差别。
注意
测试技巧 中包含了许多可以拷贝粘贴的示例。
举个例子,比如我们有这么个计数器组件:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
You clicked {count} times
);
}
我们会使用 React DOM 来测试它。为了确保它表现得和在浏览器中一样,我们会把代码渲染的部分包裹起来,并更新为 ReactTestUtils.act()
调用:
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';import Counter from './Counter';
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('can render and update a counter', () => {
// 测试首次渲染和 effect
act(() => { ReactDOM.render( , container); }); const button = container.querySelector('button');
const label = container.querySelector('p');
expect(label.textContent).toBe('You clicked 0 times');
expect(document.title).toBe('You clicked 0 times');
// 测试第二次渲染和 effect
act(() => { button.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(label.textContent).toBe('You clicked 1 times');
expect(document.title).toBe('You clicked 1 times');
});
对 act()
的调用也会清空它们内部的 effect。
如果你需要测试一个自定义 Hook,你可以在你的测试代码中创建一个组件并在其中使用你的 Hook。然后你就可以测试你刚写的组件了。
为了减少不必要的模板项目,我们推荐使用 React Testing Library,该项目旨在鼓励你按照终端用户使用组件的方式来编写测试。
欲了解更多,请参阅测试技巧一节。
我们提供了一个 ESLint 插件 来强制 Hook 规范 以避免 Bug。它假设任何以 「use
」 开头并紧跟着一个大写字母的函数就是一个 Hook。我们知道这种启发方式并不完美,甚至存在一些伪真理,但如果没有一个全生态范围的约定就没法让 Hook 很好的工作 —— 而名字太长会让人要么不愿意采用 Hook,要么不愿意遵守约定。
规范尤其强制了以下内容:
大驼峰法
命名的函数(视作一个组件)内部,要么在另一个 useSomething
函数(视作一个自定义 Hook)中。还有一些其他的启发方式,但随着我们不断地调优以在发现 Bug 和避免伪真理之前取得平衡,这些方式随时会改变。
constructor
:函数组件不需要构造函数。你可以通过调用 useState
来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState
。getDerivedStateFromProps
:改为 在渲染时 安排一次更新。shouldComponentUpdate
:详见 下方 React.memo
.render
:这是函数组件体本身。componentDidMount
, componentDidUpdate
, componentWillUnmount
:useEffect
Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。getSnapshotBeforeUpdate
,componentDidCatch
以及 getDerivedStateFromError
:目前还没有这些方法的 Hook 等价写法,但很快会被添加。该 demo 会帮助你开始理解。欲了解更多,请查阅 此文章 来了解如何使用 Hook 进行数据获取。
有!useRef()
Hook 不仅可以用于 DOM refs。「ref」 对象是一个 current
属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。
你可以在 useEffect
内部对其进行写入:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id; return () => {
clearInterval(intervalRef.current);
};
});
// ...
}
如果我们只是想设定一个循环定时器,我们不会需要这个 ref(id
可以是在 effect 本地的),但如果我们想要在一个事件处理器中清除这个循环定时器的话这就很有用了:
// ...
function handleCancelClick() {
clearInterval(intervalRef.current); }
// ...
从概念上讲,你可以认为 refs 就像是一个 class 的实例变量。除非你正在做 懒加载,否则避免在渲染期间设置 refs —— 这可能会导致意外的行为。相反的,通常你应该在事件处理器和 effects 中修改 refs。
如果你之前用过 class,你或许会试图总是在一次 useState()
调用中传入一个包含了所有 state 的对象。如果你愿意的话你可以这么做。这里有一个跟踪鼠标移动的组件的例子。我们在本地 state 中记录它的位置和尺寸:
function Box() {
const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
// ...
}
现在假设我们想要编写一些逻辑以便在用户移动鼠标时改变 left
和 top
。注意到我们是如何必须手动把这些字段合并到之前的 state 对象的:
// ...
useEffect(() => {
function handleWindowMouseMove(e) {
// 展开 「...state」 以确保我们没有 「丢失」 width 和 height setState(state => ({ ...state, left: e.pageX, top: e.pageY })); }
// 注意:这是个简化版的实现
window.addEventListener('mousemove', handleWindowMouseMove);
return () => window.removeEventListener('mousemove', handleWindowMouseMove);
}, []);
// ...
这是因为当我们更新一个 state 变量,我们会 替换 它的值。这和 class 中的 this.setState
不一样,后者会把更新后的字段 合并 入对象中。
如果你错过自动合并,你可以写一个自定义的 useLegacyState
Hook 来合并对象 state 的更新。然而,我们推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。
举个例子,我们可以把组件的 state 拆分为 position
和 size
两个对象,并永远以非合并的方式去替换 position
:
function Box() {
const [position, setPosition] = useState({ left: 0, top: 0 }); const [size, setSize] = useState({ width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.pageX, top: e.pageY }); }
// ...
把独立的 state 变量拆分开还有另外的好处。这使得后期把一些相关的逻辑抽取到一个自定义 Hook 变得容易,比如说:
function Box() {
const position = useWindowPosition(); const [size, setSize] = useState({ width: 100, height: 100 });
// ...
}
function useWindowPosition() { const [position, setPosition] = useState({ left: 0, top: 0 });
useEffect(() => {
// ...
}, []);
return position;
}
注意看我们是如何做到不改动代码就把对 position
这个 state 变量的 useState
调用和相关的 effect 移动到一个自定义 Hook 的。如果所有的 state 都存在同一个对象中,想要抽取出来就比较难了。
把所有 state 都放在同一个 useState
调用中,或是每一个字段都对应一个 useState
调用,这两方式都能跑通。当你在这两个极端之间找到平衡,然后把相关 state 组合到几个独立的 state 变量时,组件就会更加的可读。如果 state 的逻辑开始变得复杂,我们推荐 用 reducer 来管理它,或使用自定义 Hook。
这是个比较罕见的使用场景。如果你需要的话,你可以 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。(如果你发现自己经常在这么做,你可以为之创建一个自定义 Hook。)
目前,你可以 通过 ref 来手动实现:
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; });
const prevCount = prevCountRef.current;
return Now: {count}, before: {prevCount}
;
}
这或许有一点错综复杂,但你可以把它抽取成一个自定义 Hook:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count); return Now: {count}, before: {prevCount}
;
}
function usePrevious(value) { const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
注意看这是如何作用于 props, state,或任何其他计算出来的值的。
function Counter() {
const [count, setCount] = useState(0);
const calculation = count + 100;
const prevCalculation = usePrevious(calculation); // ...
考虑到这是一个相对常见的使用场景,很可能在未来 React 会自带一个 usePrevious
Hook。
参见 derived state 推荐模式.
组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。例如,考虑这样的代码:
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
You clicked {count} times
);
}
如果你先点击「Show alert」然后增加计数器的计数,那这个 alert 会显示在你点击『Show alert』按钮时的 count
变量。这避免了那些因为假设 props 和 state 没有改变的代码引起问题。
如果你刻意地想要从某些异步回调中读取 最新的 state,你可以用 一个 ref 来保存它,修改它,并从中读取。
最后,你看到陈旧的 props 和 state 的另一个可能的原因,是你使用了「依赖数组」优化但没有正确地指定所有的依赖。举个例子,如果一个 effect 指定了 []
作为第二个参数,但在内部读取了 someProp
,它会一直「看到」 someProp
的初始值。解决办法是要么移除依赖数组,要么修正它。 这里介绍了 你该如何处理函数,而这里介绍了关于如何减少 effect 的运行而不必错误的跳过依赖的 一些常见策略。
注意
我们提供了一个
exhaustive-deps
ESLint 规则作为eslint-plugin-react-hooks
包的一部分。它会在依赖被错误指定时发出警告,并给出修复建议。
getDerivedStateFromProps
?尽管你可能 不需要它,但在一些罕见的你需要用到的场景下(比如实现一个
组件),你可以在渲染过程中更新 state 。React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。
这里我们把 row
prop 上一轮的值存在一个 state 变量中以便比较:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row 自上次渲染以来发生过改变。更新 isScrollingDown。
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
初看这或许有点奇怪,但渲染期间的一次更新恰恰就是 getDerivedStateFromProps
一直以来的概念。
如果前后两次的值相同,useState
和 useReducer
Hook 都会放弃更新。原地修改 state 并调用 setState
不会引起重新渲染。
通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
可能的话尽量避免这种模式。
尽管你不应该经常需要这么做,但你可以通过 useImperativeHandle
Hook 暴露一些命令式的方法给父组件。
获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。这里有一个 小 demo:
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []);
return (
<>
Hello, world
The above header is {Math.round(height)}px tall
>
);
}
在这个案例中,我们没有选择使用 useRef
,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。
注意到我们传递了 []
作为 useCallback
的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。
In this example, the callback ref will be called only when the component mounts and unmounts, since the rendered component stays present throughout any rerenders. If you want to be notified any time a component resizes, you may want to use
ResizeObserver
or a third-party Hook built on it.
如果你愿意,你可以 把这个逻辑抽取出来作为 一个可复用的 Hook:
function MeasureExample() {
const [rect, ref] = useClientRect(); return (
<>
Hello, world
{rect !== null &&
The above header is {Math.round(rect.height)}px tall
}
>
);
}
function useClientRect() {
const [rect, setRect] = useState(null);
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
}
const [thing, setThing] = useState()
是什么意思?如果你不熟悉这个语法,可以查看 State Hook 文档中的 解释 一节。
可以的。参见 条件式的发起 effect。注意,忘记处理更新常会 导致 bug,这也正是我们没有默认使用条件式 effect 的原因。
一般来说,不安全。
function Example({ someProp }) {
function doSomething() {
console.log(someProp); }
useEffect(() => {
doSomething();
}, []); // 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)}
要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp); }
doSomething();
}, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)}
如果这样之后我们依然没用到组件作用域中的任何值,就可以安全地把它指定为 []
:
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []); // ✅ 在这个例子中是安全的,因为我们没有用到组件作用域中的 *任何* 值
根据你的用例,下面列举了一些其他的办法。
注意
我们提供了一个
exhaustive-deps
ESLint 规则作为eslint-plugin-react-hooks
包的一部分。它会帮助你找出无法一致地处理更新的组件。
让我们来看看这有什么关系。
如果你指定了一个 依赖列表 作为 useEffect
、useMemo
、useCallback
或 useImperativeHandle
的最后一个参数,它必须包含回调中的所有值,并参与 React 数据流。这就包括 props、state,以及任何由它们衍生而来的东西。
只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。下面这个案例有一个 Bug:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId); // 使用了 productId prop const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 这样是无效的,因为 `fetchProduct` 使用了 `productId` // ...
}
推荐的修复方案是把那个函数移动到你的 effect 内部。这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。 async function fetchProduct() { const response = await fetch('http://myapi/product/' + productId); const json = await response.json(); setProduct(json); }
fetchProduct();
}, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId // ...
}
这同时也允许你通过 effect 内部的局部变量来处理无序的响应:
useEffect(() => {
let ignore = false; async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
if (!ignore) setProduct(json); }
fetchProduct();
return () => { ignore = true }; }, [productId]);
我们把这个函数移动到 effect 内部,这样它就不用出现在它的依赖列表中了。
提示
看看 这个小 demo 和 这篇文章 来了解更多关于如何用 Hook 进行数据获取。
如果处于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法:
useCallback
Hook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变:function ProductPage({ productId }) {
// ✅ 用 useCallback 包裹以避免随渲染发生改变 const fetchProduct = useCallback(() => { // ... Does something with productId ... }, [productId]); // ✅ useCallback 的所有依赖都被指定了
return ;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
// ...
}
注意在上面的案例中,我们 需要 让函数出现在依赖列表中。这确保了 ProductPage
的 productId
prop 的变化会自动触发 ProductDetails
的重新获取。
有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state }, 1000);
return () => clearInterval(id);
}, []); // Bug: `count` 没有被指定为依赖
return {count}
;
}
传入空的依赖数组 []
,意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval
的回调中,count
的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count
的值被保存在该闭包当中,且初值为 0
。每隔一秒,回调就会执行 setCount(0 + 1)
,因此,count
永远不会超过 1。
指定 [count]
作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval
在被清除前(类似于 setTimeout
)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState
的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量 }, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不适用组件作用域中的任何变量
return {count}
;
}
(setCount
函数的身份是被确保稳定的,所以可以放心的省略掉)
此时,setInterval
的回调依旧每秒调用一次,但每次 setCount
内部的回调取到的 count
是最新值(在回调中变量命名为 c
)。
在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer
Hook 把 state 更新逻辑移到 effect 之外。这篇文章 提供了一个你该如何做到这一点的案例。 useReducer
的 dispatch
的身份永远是稳定的 —— 即使 reducer 函数是定义在组件内部并且依赖 props。
万不得已的情况下,如果你想要类似 class 中的 this
的功能,你可以 使用一个 ref 来保存一个可变的变量。然后你就可以对它进行读写了。举个例子:
function Example(props) {
// 把最新的 props 保存在一个 ref 中 const latestProps = useRef(props); useEffect(() => { latestProps.current = props; });
useEffect(() => {
function tick() {
// 在任何时候读取最新的 props console.log(latestProps.current); }
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // 这个 effect 从不会重新执行}
仅当你实在找不到更好办法的时候才这么做,因为依赖于变更会使得组件更难以预测。如果有某些特定的模式无法很好地转化成这样,发起一个 issue 并配上可运行的实例代码以便,我们会尽可能帮助你。
shouldComponentUpdate
?你可以用 React.memo
包裹一个组件来对它的 props 进行浅比较:
const Button = React.memo((props) => {
// 你的组件
});
这不是一个 Hook 因为它的写法和 Hook 不同。React.memo
等效于 PureComponent
,但它只比较 props。(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)
React.memo
不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用 useMemo
优化每一个具体的子节点。
useMemo
Hook 允许你通过「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
这行代码会调用 computeExpensiveValue(a, b)
。但如果依赖数组 [a, b]
自上次赋值以来没有改变过,useMemo
会跳过二次调用,只是简单复用它上一次返回的值。
记住,传给 useMemo
的函数是在渲染期间运行的。不要在其中做任何你通常不会在渲染期间做的事。举个例子,副作用属于 useEffect
,而不是 useMemo
。
你可以把 useMemo
作为一种性能优化的手段,但不要把它当做一种语义上的保证。未来,React 可能会选择「忘掉」一些之前记住的值并在下一次渲染时重新计算它们,比如为离屏组件释放内存。建议自己编写相关代码以便没有 useMemo
也能正常工作 —— 然后把它加入性能优化。(在某些取值必须 从不 被重新计算的罕见场景,你可以 惰性初始化 一个 ref。)
方便起见,useMemo
也允许你跳过一次子节点的昂贵的重新渲染:
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => , [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => , [b]);
return (
<>
{child1}
{child2}
>
)
}
注意这种方式在循环中是无效的,因为 Hook 调用 不能 被放在循环中。但你可以为列表项抽取一个单独的组件,并在其中调用 useMemo
。
如果依赖数组的值相同,useMemo
允许你 记住一次昂贵的计算。但是,这仅作为一种提示,并不 保证 计算不会重新运行。但有时候需要确保一个对象仅被创建一次。
第一个常见的使用场景是当创建初始 state 很昂贵时:
function Table(props) {
// ⚠️ createRows() 每次渲染都会被调用
const [rows, setRows] = useState(createRows(props.count));
// ...
}
为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState
:
function Table(props) {
// ✅ createRows() 只会被调用一次
const [rows, setRows] = useState(() => createRows(props.count));
// ...
}
React 只会在首次渲染时调用这个函数。参见 useState
API 参考。
你或许也会偶尔想要避免重新创建 useRef()
的初始值。举个例子,或许你想确保某些命令式的 class 实例只被创建一次:
function Image(props) {
// ⚠️ IntersectionObserver 在每次渲染都会被创建
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
}
useRef
不会 像 useState
那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的:
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver 只会被惰性创建一次
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// 当你需要时,调用 getObserver()
// ...
}
这避免了我们在一个对象被首次真正需要之前就创建它。如果你使用 Flow 或 TypeScript,你还可以为了方便给 getObserver()
一个不可为 null 的类型。
不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
除此之外,可以认为 Hook 的设计在某些方面更加高效:
传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate
优化有关。Hook 从三个方面解决了这个问题。
useCallback
Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate
继续工作:
// 除非 `a` 或 `b` 改变,否则不会变
const memoizedCallback = useCallback(() => { doSomething(a, b);
}, [a, b]);
useMemo
Hook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要。useReducer
Hook 减少了对深层传递回调的依赖,正如下面解释的那样。我们已经发现大部分人并不喜欢在组件树的每一层手动传递回调。尽管这种写法更明确,但这给人感觉像错综复杂的管道工程一样麻烦。
在大型的组件树中,我们推荐的替代方案是通过 context 用 useReducer
往下传一个 dispatch
函数:
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 提示:`dispatch` 不会在重新渲染之间变化 const [todos, dispatch] = useReducer(todosReducer);
return (
);
}
TodosApp
内部组件树里的任何子节点都可以使用 dispatch
函数来向上传递 actions 到 TodosApp
:
function DeepChild(props) {
// 如果我们想要执行一个 action,我们可以从 context 中获取 dispatch。 const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
);
}
总而言之,从维护的角度来这样看更加方便(不用不断转发回调),同时也避免了回调的问题。像这样向下传递 dispatch
是处理深度更新的推荐模式。
注意,你依然可以选择是要把应用的 state 作为 props 向下传递(更显明确)还是作为作为 context(对很深的更新而言更加方便)。如果你也使用 context 来向下传递 state,请使用两种不同的 context 类型 —— dispatch
context 永远不会变,因此组件通过读取它就不需要重新渲染了,除非它们还需要应用的 state。
useCallback
读取一个经常变化的值?注意
我们推荐 在 context 中向下传递
dispatch
而非在 props 中使用独立的回调。下面的方法仅仅出于文档完整性考虑,以及作为一条出路在此提及。同时也请注意这种模式在 并行模式 下可能会导致一些问题。我们计划在未来提供一个更加合理的替代方案,但当下最安全的解决方案是,如果回调所依赖的值变化了,总是让回调失效。
在某些罕见场景中,你可能会需要用 useCallback
记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好。如果你想要记住的函数是一个事件处理器并且在渲染期间没有被用到,你可以 把 ref 当做实例变量 来用,并手动把最后提交的值保存在它当中:
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useEffect(() => {
textRef.current = text; // 把它写入 ref });
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // 从 ref 读取它 alert(currentText);
}, [textRef]); // 不要像 [text] 那样重新创建 handleSubmit
return (
<>
updateText(e.target.value)} />
>
);
}
这是一个比较麻烦的模式,但这表示如果你需要的话你可以用这条出路进行优化。如果你把它抽取成一个自定义 Hook 的话会更加好受些:
function Form() {
const [text, updateText] = useState('');
// 即便 `text` 变了也会被记住:
const handleSubmit = useEventCallback(() => { alert(text);
}, [text]);
return (
<>
updateText(e.target.value)} />
>
);
}
function useEventCallback(fn, dependencies) { const ref = useRef(() => {
throw new Error('Cannot call an event handler while rendering.');
});
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
无论如何,我们都 不推荐使用这种模式 ,只是为了文档的完整性而把它展示在这里。相反的,我们更倾向于 避免向下深入传递回调。
React 保持对当先渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState()
调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState()
调用会得到各自独立的本地 state 的原因。
Hook 由不同的来源的多个想法构成:
adopt
关键字 作为 render props 的语法糖的提案。Sebastian Markbåge 想到了 Hook 最初的设计,后来经过 Andrew Clark,Sophie Alpert,Dominic Gannaway,和 React 团队的其它成员的提炼。