Hooks
16.8新增,Hook 是一些可以让你在函数组件里钩入React state、生命周期等特性的函数,它们名字通常都以 use 开始。
Hook 不能在 class 组件中使用
Hook相比class解决的问题:不相关逻辑的分离、相关逻辑的合并
- class 中生命周期函数中经常包含不相关的逻辑(对state不同属性的操作),又把相关逻辑分离到了几个不同声明周期中
- 而对于Hook,可以使用多个 state Hook 和 多个effect Hook,将不相关逻辑分离到不同的 effect 中,将相关的逻辑写到一个effect中
通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。
React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
Hook 使用规则
Hook 是如何工作的。在多次渲染间,React如何知道哪个 useState 调用对应于哪个 state 变量?React 又是如何匹配前后多次渲染中的每一个 effect 的
React 靠的是 Hook 调用的顺序
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。
Hook 就是JS函数,但是使用Hook有2个额外的规则:
1. 只在函数组件最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook
确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确
如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:1
2
3
4
5
6useEffect(function persistForm() {
// 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});
2. 只能在 React 的函数组件中调用
- 在 React 的函数组件中调用 Hook
- 在自定义 Hook 中调用其他 Hook
State Hook
React 提供的 useState Hook,有时候我们也叫它 State Hook。它让我们在 React 函数组件上添加内部 state
1 | const [state, setState] = useState(initialState) |
useState 返回一个含有两个元素的数组:当前state,更新 state 的函数(利用数组解构的方法获取)
在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。
useState 唯一的参数就是初始 state,不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。
useState Hook 类似 class 组件的 this.setState,但是它useState不会把新的 state 和旧的 state 进行合并,可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果
1 | setState(prevState => { |
useReducer 是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。
useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就就会消失,而 state 中的变量会被 React 保留
如果我们想要在 state 中存储多个不同的变量,只需多次调用 useState()1
2
3
4
5
6
7
8
9
10
11
12
13
14import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量。
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>
</button>
</div>
);
}
惰性初始state
initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数
,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
Effect Hook
副作用函数
Effect Hook可以让你能在函数组件中执行副作用操作,和class组件中的生命周期函数类似,通过使用这个 Hook,可以告诉 React 组件需要在渲染后执行某些操作,React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。
useEffect 就是一个 Effect Hook,和class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API,可以看做三个函数的组合
默认情况下,React 会在每次渲染后调用useEffect(第一次渲染和每次更新之后),React 保证了每次运行 effect 的时,DOM 都已经更新完毕。
副作用函数还可以通过返回一个函数来指定如何“清除”副作用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `${count} times`
})
return (
<div>
<p>{count} times</p>
<button onClick={() => setCount(count + 1)}>
</button>
</div>
);
}
需要清除的Effect
组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源,useEffect 函数可以返回一个清除函数,,清除函数会在组件卸载前执行。
如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除,帮助我们创建 bug 更少的组件1
2
3
4
5
6
7useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。
通过跳过 Effect 进行性能优化
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决
这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数,如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。1
2
3
4useEffect(() => {
document.title = `${count} times`
}, [count])
// 仅在 count 更改时更新
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。
effect 的执行时机
与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)
自定义Hook
在组件之间重用一些状态逻辑:高阶组件和 render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。自定义 Hook 可以将 React 中提供的 Hook 组合到定制的 Hook 中,以复用不同组件之间常见的状态逻辑。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
1 | import React, { useState, useEffect } from 'react' |
1 | function FriendStatus(props) { |
1 | function FriendListItem(props) { |
useContext
useContext让你不使用组件嵌套就可以订阅React的Context1
const value = useContext(MyContext);
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
调用了 useContext 的组件总会在 context 值变化时重新渲染。
useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
1 | function Example() { |
另外 useReducer 可以让你通过 reducer 来管理组件本地的复杂 state。1
2
3function Todos() {
const [todos, dispatch] = useReducer(todosReducer);
// ...
其他Hook
useReducer
编写一个 useReducer 的 Hook,使用 reducer 的方式来管理组件的内部 state1
2
3
4
5
6
7
8
9
10function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
在组件中使用它,让 reducer 驱动它管理 state:1
2
3
4
5
6
7
8
9function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: 'add', text });
}
// ...
}