# React@16.8 Hooks
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数,它们的名字通常以use
开头。
Hook 不能在 class 组件中使用 — 这使得你不使用 class 也能使用 React。
只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用,否则会导致bug产生。
只能在 React 的函数组件中(或者自定义的 Hook 中)调用 Hook,不要在其他 JavaScript 函数中调用。
Hooks Api
- 基础的 Hook
- useState
- useEffect
- useContext
- 额外的 Hook
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect - 测量布局
- useDebugValue
了解更多:
# useState
useState 又叫 State Hook。
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量,
// 声明一个叫 "setCount" 的更新函数,类似于 this.setState
const [count, setCount] = useState(0);
// 或者
const [count2, setCount2] = useState({number: 0})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
{/*坑:这种写法可以防止 count 被缓存起来【一直都是初始的{number: 0}】*/}
<button onClick={() => setCount2(count2 => count2 + 1)}>
Click me2
</button>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
setCount 不会把新的 state 和 旧的 state 合并。
setCount((prevState) => ({
...prevState,
left: e.pageX,
top: e.pageY,
}))
2
3
4
5
# useEffect
useEffect 又叫 Effect Hook。简单理解为依赖 [deps]
变化的作用。
其相当于 componentDidMount
、 componentDidUpdate
、componentWillUnmount
。
# 无需清除的 effect
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 需要清除的 effect
如果你的 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';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
当然这不是必须的。
通过跳过某些 effect 进行性能优化。可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
2
3
上面这个示例中,我们传入 count 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 5 和后一次渲染的 5 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。
如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。
// 只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。
useEffect(() => {
document.title = `You clicked many times`;
}, []); // 仅在组件挂载和卸载时执行
2
3
4
像这种接受依赖数组的除了useEffect
以外,还有 useMemo , useCallback , useImperativeHandle 。
# useContext
useContext 让你不使用组件嵌套就可以订阅 React 的 Context。
# 创建 context
const Context = React.createContext(0);
# 提供 context
function conpoment () {
return <Context.Provider value={0}></Context.Provider>
}
2
3
# 获取 context
const count = useContext(Context)
# useReducer
useReducer 可以让你通过 reducer 来管理组件本地的复杂 state。
# 定义 userReducer
const reducer = (state, action) => {
switch (action.type) {
case "add":
return state + 1;
case "sub":
return state - 1;
}
}
const [counter, dispath] = useReducer(reducer, 0)
2
3
4
5
6
7
8
9
10
# useMemo
useMemo 缓存(另外一个useCallback)。当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数
传入 useMemo 的函数会在渲染期间执行,而 useEffect 只能在DOM更新后再触发,所以使用 useMemo 就能解决"怎么在DOM改变的时候,控制某些函数不被触发"。
和 useMemo 相近的还有一个 useCallback,只是后者返回一个函数 useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
简言之 useEffect 是在渲染之后完成的, useMemo 是在渲染期间完成的。而 useMemo(() => fn, deps) 等价于 useCallback(fn, deps)
import React, {Fragment} from 'react'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { observer } from 'mobx-react'
const nameList = ['apple', 'peer', 'banana', 'lemon']
const Example = observer((props) => {
const [price, setPrice] = useState(0)
const [name, setName] = useState('apple')
function getProductName() {
console.log('getProductName触发')
return name
}
// 只对name响应
useEffect(() => {
console.log('name effect 触发')
getProductName()
}, [name])
// 只对price响应
useEffect(() => {
console.log('price effect 触发')
}, [price])
// memo化的name属性
const memo_name = useMemo(() => {
console.log('name memo 触发')
return () => name // 返回一个函数
}, [name])
return (
<Fragment>
<p>{name}</p>
<p>{price}</p>
<p>普通的name:{getProductName()}</p>
<p>memo化的:{memo_name()}</p>
<button onClick={() => setPrice(price+1)}>价钱+1</button>
<button onClick={() => setName(nameList[Math.random() * nameList.length << 0])}>修改名字</button>
</Fragment>
)
})
export default Example
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
点击价钱 +1 按钮会发生什么 ?
> getProductName触发
> price effect 触发
// 首先DOM改变,触发在p标签中的getProductName函数
// 然后调用effect
2
3
4
5
显然我们已经成功的控制了触发(没有触发memo_name),这也是官方为什么说不能在useMemo中操作DOM之类的副作用操作,不要在这个函数内部执行与渲染无关的 操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo,你可以试一下,在useMemo中使用setState你会发现会产生死循环,并且会有警 告,因为useMemo是在渲染中进行的,你在其中操作DOM后,又会导致触发memo
点击修改名字按钮会发生什么 ?
> name memo 触发
> getProductName触发
> name effect 触发
> getProductName触发
2
3
4
从这里也可以看出,memo是在DOM更新前触发的,就像官方所说的,类比生命周期就是shouldComponentUpdate
# useRef
import React, { useState, useEffect, useMemo, useRef } from 'react';
export default function App(props){
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => {
return 2 * count;
}, [count]);
const couterRef = useRef();
useEffect(() => {
document.title = `The value is ${count}`;
console.log(couterRef.current);
}, [count]);
return (
<>
<button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
</>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
代码中用 useRef 创建了 couterRef 对象,并将其赋给了 button 的 ref 属性。这样,通过访问 couterRef.current 就可以访问到 button 对应的DOM对象。
# 自定义 Hook
自定义 Hook 是一种复用状态逻辑的方式,它不复用 state 本身。
事实上 Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。
自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook,自定义 Hook 内部可以调用其他 Hook。
# 使用单个 state,还是多个 state
答:
使用多个 state 变量可以让 state 的粒度更细,更易于逻辑的拆分和组合,将完全不相关的 state 拆分为多组 state。
如果某些 state 是相互关联的,或者需要一起发生改变,就可以把它们合并为一组 state。
# deps 依赖过多,导致 Hooks 难以维护?
答:
依赖数组依赖的值最好不要超过 3 个,否则会导致代码会难以维护。如果发现依赖数组依赖的值过多,我们应该采取一些方法来减少它:
- 去掉不必要的依赖。
- 将 Hook 拆分为更小的单元,每个 Hook 依赖于各自的依赖数组。
- 通过合并相关的 state,将多个依赖值聚合为一个。
- 通过 setState 回调函数获取最新的 state,以减少外部依赖。
- 通过 ref 来读取可变变量的值,不过需要注意控制修改它的途径。
# 使用 Hooks 时还有哪些好的实践?
- 若 Hook 类型相同,且依赖数组一致时,应该合并成一个 Hook。否则会产生更多开销。
const dataA = useMemo(() => {
return getDataA();
}, [A, B]);
const dataB = useMemo(() => {
return getDataB();
}, [A, B]);
// 应该合并为
const [dataA, dataB] = useMemo(() => {
return [getDataA(), getDataB()]
}, [A, B]);
2
3
4
5
6
7
8
9
10
11
12
- 参考原生 Hooks 的设计,自定义 Hooks 的返回值可以使用 Tuple 类型,更易于在外部重命名。但如果返回值的数量超过三个,还是建议返回一个对象。
export const useToggle = (defaultVisible: boolean = false) => {
const [visible, setVisible] = useState(defaultVisible);
const show = () => setVisible(true);
const hide = () => setVisible(false);
return [visible, show, hide] as [typeof visible, typeof show, typeof hide];
};
const [isOpen, open, close] = useToggle(); // 在外部可以更方便地修改名字
const [visible, show, hide] = useToggle();
2
3
4
5
6
7
8
9
10
ref 不要直接暴露给外部使用,而是提供一个修改值的方法。
在使用 useMemo 或者 useCallback 时,确保返回的函数只创建一次。也就是说,函数不会根据依赖数组的变化而二次创建。举个例子:
export const useCount = () => {
const [count, setCount] = useState(0);
const [increase, decrease] = useMemo(() => {
const increase = () => {
setCount(count + 1);
};
const decrease = () => {
setCount(count - 1);
};
return [increase, decrease];
}, [count]);
return [count, increase, decrease];
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在 useCount Hook 中, count 状态的改变会让 useMemo 中的 increase 和 decrease 函数被重新创建。由于闭包特性,如果这两个函数被其他 Hook 用 到了,我们应该将这两个函数也添加到相应 Hook 的依赖数组中,否则就会产生 bug。比如:
function Counter() {
const [count, increase] = useCount();
useEffect(() => {
const handleClick = () => {
increase(); // 执行后 count 的值永远都是 1
};
document.body.addEventListener("click", handleClick);
return () => {
document.body.removeEventListener("click", handleClick);
};
}, []);
return <h1>{count}</h1>;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在 useCount 中,increase 会随着 count 的变化而被重新创建。但是 increase 被重新创建之后, useEffect 并不会再次执行,所以 useEffect 中取到 的 increase 永远都是首次创建时的 increase 。而首次创建时 count 的值为 0,因此无论点击多少次, count 的值永远都是 1。
那把 increase 函数放到 useEffect 的依赖数组中不就好了吗?事实上,这会带来更多问题:
- increase 的变化会导致频繁地绑定事件监听,以及解除事件监听。
- 需求是只在组件 mount 时执行一次 useEffect,但是 increase 的变化会导致 useEffect 多次执行,不能满足需求。
如何解决这些问题呢?
一、通过 setState 回调,让函数不依赖外部变量。例如:
export const useCount = () => {
const [count, setCount] = useState(0);
const [increase, decrease] = useMemo(() => {
const increase = () => {
setCount((latestCount) => latestCount + 1);
};
const decrease = () => {
setCount((latestCount) => latestCount - 1);
};
return [increase, decrease];
}, []); // 保持依赖数组为空,这样 increase 和 decrease 方法都只会被创建一次
return [count, increase, decrease];
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
二、通过 ref 来保存可变变量。例如:
export const useCount = () => {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
});
const [increase, decrease] = useMemo(() => {
const increase = () => {
setCount(countRef.current + 1);
};
const decrease = () => {
setCount(countRef.current - 1);
};
return [increase, decrease];
}, []); // 保持依赖数组为空,这样 increase 和 decrease 方法都只会被创建一次
return [count, increase, decrease];
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21