某人

此前素未谋面、此后遥遥无期

0%

React Hooks

React Hooks

HookReact 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

  1. 只在最顶层使用 Hook
  2. 只在 React 函数中调用 Hook

基础 Hook

  1. useState
  2. useEffect
  3. useContext

额外的 Hook

  1. useReducer
  2. useCallback
  3. useMemo
  4. useRef
  5. useImperativeHandle
  6. useLayoutEffect
  7. useDebugValue

注意

React 16.8.0 是第一个支持 Hook 的版本。升级时,请注意更新所有的 package,包括 React DOM React Native0.59 版本开始支持 Hook

ESLint 插件

一个名为 eslint-plugin-react-hooksESLint 插件来强制执行这两条规则。如果你想尝试一下,可以将此插件添加到你的项目中:

1
npm install eslint-plugin-react-hooks --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}

后续版本默认添加此插件到 Create React App 及其他类似的工具包中。

useState

useState 是最基本的 API,它传入一个初始值,每次函数执行都能拿到新值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState } from 'react';
import styles from './index.module.scss';
function PageCount() {

const [count, setCount] = useState(0);

return (
<div className={styles.PageMain}>
<button onClick={() => setCount(count + 1)}>+</button>
<h1>{count}</h1>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
);
}
export default PageCount;

useReducer

useReduceruseState 几乎是一样的,需要外置外置 reducer (全局),通过这种方式可以对多个状态同时进行控制。仔细端详起来,其实跟 redux 中的数据流的概念非常接近。

1
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
import React, { useReducer } from 'react';
import styles from './index.module.scss';

function reducer(state, action) {
switch (action.type) {
case 'up':
return { count: state.count + 1 };
case 'down':
return { count: state.count - 1 };
default:
return ""
}
}

function PageCount() {
const [state, dispatch] = useReducer(reducer, { count: 1 })
return (
<div className={styles.PageMain}>
<button onClick={() => dispatch({ type: 'up' })}>+</button>
<h1>{state.count}</h1>
<button onClick={() => dispatch({ type: 'down' })}>-</button>
</div>
);
}

export default PageCount;

useEffect

React hook 中,useEffect 用来取代 componentDidMountcomponentDidUpdate。主要作用是当页面渲染后,进行一些副作用操作(比如访问 DOM,请求数据)。

数据获取,设置订阅或者手动直接更改 React 组件中的 DOM 都属于副作用。有的人习惯成这种行为为 effect

Effect Hook 可以让你能够在 Function 组件中执行副作用

React 组件中有两种常见的副作用:

  1. 需要清理的副作用
  2. 不需要清理的副作用。

如果在重新渲染之间没有更新某些值,则可以告诉 React 跳过 effect ,为了实现这种方式,需要将数组作为可选的第二个参数传递给 useEffect

effect 中返回一个 function ,这是 effect 可选的清理机制。每个 effect 都可以返回一个在它之后清理的 function

  • useEffect最后,不加[]就表示每一次渲染都执行
  • useEffect最后,加了[]就表示只第一次执行
  • useEffect最后,加[],并且[]里面加的字段就表示,这个字段更改了,我这个effect才执行
  • useEffect不能被判断包裹
  • useEffect不能被打断
1
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
import React, { useState, useEffect } from 'react';
import styles from './index.module.scss';

function PageCount() {
const [count, setCount] = useState(0);

// => componentDidMount/componentDidUpdate
useEffect(() => {
// update
document.title = `You clicked ${count} times`;
// => componentWillUnMount
return function cleanup() {
document.title = 'app';
}
}, [count]);


return (
<div className={styles.PageMain}>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}> Click me </button>
</div>
);
}

export default PageCount;

React 何时清除 effect?

React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。

这将助于避免 bug以及如何在遇到性能问题时跳过此行为。

useMemo

useMemo缓存计算数据的值

useMemo 主要用于渲染过程优化,两个参数依次是计算函数(通常是组件函数)和依赖状态列表,当依赖的状态发生改变时,才会触发计算函数的执行。如果没有指定依赖,则每一次渲染过程都会执行该计算函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState, useMemo } from 'react';
import styles from './index.module.scss';

export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);

return <div className={styles.PageMain}>
<h4>{count}-{expensive}</h4>
<p>{val}</p>
<div>
<button onClick={() => setCount(count + 1)}>click me +1</button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</div>;
}

useContext

useContextuseReducer的使用

调用useContext,传入从React.createContext获取的上下文对象。

1
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React, { createContext, useContext, useReducer } from "react";
//默认状态
export const defaultState = {
value: 0
}

//reducer
export function reducer(state, action) {
switch (action.type) {
case 'ADD_NUM':
return { ...state, value: state.value + 1 };
case 'REDUCE_NUM':
return { ...state, value: state.value - 1 };
default:
throw new Error();
}
}

//存储
const StoreContext = createContext(null);

//子组件一
export function ChildFirst() {
const AppContext = useContext(StoreContext)
return (
<div>
<button onClick={() => {
AppContext.dispatch({
type: "ADD_NUM",
payload: {}
})
}}>增加</button>
<button onClick={() => {
AppContext.dispatch({
type: "REDUCE_NUM",
payload: {}
})
}}>递减</button>
</div>
)
}

//子组件二
export function ChildSecond() {
const AppContext = useContext(StoreContext)
return (
<div>
{AppContext.state.value + 's'}
</div>
)
}

//其他页面只需要引入根组件
export function RootCompent() {
const [state, dispatch] = useReducer(reducer, defaultState)

return (
<StoreContext.Provider value={{ state, dispatch: dispatch }}>
<ChildFirst />
<ChildSecond />
</StoreContext.Provider>
)
}

useRef

在函数式组件里没有了 this 来存放一些实例的变量,所以 React 建议使用 useRef 来存放一些会发生变化的值,useRef 并不再单单是为了 DOMref 准备的,同时也会用来存放组件实例的属性。

useRef 主要有两个使用场景:

  1. 获取子组件或者 DOM 节点的句柄
  2. 渲染周期之间的共享数据的存储

createRefuseRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { useState, createRef, useRef } from "react";
export function RootCompent() {
const [renderIndex, setRenderIndex] = useState(1)
const fromUseRef = useRef();
const fromCreateRef = createRef();
if (!fromUseRef.current) {
fromUseRef.current = renderIndex;
}
if (!fromCreateRef.current) {
fromCreateRef.current = renderIndex;
}
return (
<div>
<p>当前渲染index : {renderIndex}</p>
<p>fromUseRef : {fromUseRef.current}</p>
<p>fromCreateRef {fromCreateRef.current}</p>
<button onClick={() => {
setRenderIndex(prev => prev + 1);
}}>
点击
</button>
</div>
)
}

useRef经典例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useRef } from "react";
export function RootCompent() {
const [count, setCount] = useState(1)
const lastCount = useRef(count)
function handleClick() {
setTimeout(() => {
alert("你点击了" + count + "次");
alert("你点击了" + lastCount.current + "次");
}, 1000)
}
useEffect(() => {
lastCount.current = count;
})

return (
<div>
<p>你点击了 : {count}次</p>
<button onClick={() => { setCount(count + 1) }}>点击</button>
<button onClick={() => { handleClick() }}>显示提示</button>
</div>
)
}

useRef 每次都会返回同一个引用, 所以在 useEffect 中修改的时候 ,在 alert 中也会同时被修改. 这样子, 点击的时候就可以弹出实时的 count 了.

useRef获取上一次的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useState, useRef } from "react";
export function RootCompent() {
const [count, setCount] = useState(1)
const prevtCount = useRef()

useEffect(() => {
prevtCount.current = count;
})
return (
<div>
<p>prevtCount : {prevtCount.current}次</p>
<p>你点击了 : {count}次</p>
<button onClick={() => { setCount(count + 1) }}>点击</button>
</div>
)
}

将获取上一次的值封装成自定义钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//获取以前的状态
function usePrevious(state) {
const ref = useRef();

useEffect(() => {
ref.current = state;
})
return ref.current;
}

//其他页面只需要引入功能根组件
export function RootCompent() {
const [count, setCount] = useState(1)
const prevtCount = usePrevious(count)

return (
<div>
<p>prevtCount : {prevtCount}次</p>
<p>你点击了 : {count}次</p>
<button onClick={() => { setCount(count + 1) }}>点击加</button>
<button onClick={() => { setCount(count - 1) }}>点击减</button>
</div>
)
}

useCallback

useCallback缓存函数的引用

使用useCallback去缓存我们的 handleClick

setCount方法支持接收一个函数,通过这个函数可以获取最新的state

useCallback是基于useMemo的封装。只有当依赖数组产生变化时,useCallback才会返回一个新的函数,否则始终返回第一次的传入callback

1
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//子组件
const Child = React.memo(({ onClick }) => {
return <div onClick={onClick}>click</div>;
});

//其他页面只需要引入功能根组件
export function RootCompent() {
const [count, setCount] = useState(0)

//在setCount 这样操作的时候,不用闭包中的变量,而是使用函数,获取最新的值。
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, [])

return (
<div>
<p>{count}</p>
<Child onClick={handleClick}></Child>
</div>
)
}
````

`useCallback` 的真正目的还是在于缓存了每次渲染时 `inline callback` 的实例,这样方便配合上子组件的 `shouldComponentUpdate` 或者 `React.memo` 起到减少不必要的渲染的作用。

需要不断提醒自己注意的是,在大部分 `callback` 都会是 `inline callback` 的未来,`React.memo``React.useCallback` 一定记得需要配对使用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比较也是要消耗那么一点点点的性能。

只有对于大计算量的函数来说,利用`useCallback`才能起到良好的优化效果

## `useLayoutEffect`

其函数签名与 `useEffect` 相同,但它会在所有的 `DOM` 变更之后同步调用 `effect`。可以使用它来读取 `DOM` 布局并同步触发重渲染。

在浏览器执行绘制之前,`useLayoutEffect` 内部的更新计划将被同步刷新。

### 提示

* `useLayoutEffect``componentDidMount、componentDidUpdate` 的调用阶段是一样的。但是,我们推荐你一开始先用 `useEffect`,只有当它出问题的时候再尝试使用 `useLayoutEffect`

```javascript
//当你每次点击 div, count 会更新为 0, 之后 useEffect 内又把 count 改为一串随机数。
//所以页面会先渲染成0,然后再渲染成随机数,由于更新很快,所以出现了闪烁
function RootCompent() {
const [count, setCount] = useState(0);

useEffect(() => {
if (count === 0) {
const randomNum = 10 + Math.random() * 200
setCount(10 + Math.random() * 200);
}
}, [count]);

return (
<div>
<p>{count}</p>
<div onClick={() => setCount(0)}>点击</div>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//使用useLayoutEffect之后
function RootCompent() {
const [count, setCount] = useState(0);

useLayoutEffect(() => {
if (count === 0) {
const randomNum = 10 + Math.random() * 200
setCount(10 + Math.random() * 200);
}
}, [count]);

return (
<div>
<p>{count}</p>
<div onClick={() => setCount(0)}>点击</div>
</div>
);
}

useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

提示

不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。

useDebugValue接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

1
useDebugValue(date, date => date.toDateString());

useImperativeHandle

1
useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用

1
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
function Child(props, ref) {
const kun = useRef()

const introduce = useCallback(() => {
console.log('i can sing, jump, rap, play basketball')
}, [])

useImperativeHandle(ref, () => ({
introduce: () => {
introduce()
}
}));

return (
<div ref={kun}> {props.count}</div>
)
}

const FChild = forwardRef(Child)

function RootCompent() {
const [count, setCount] = useState(0)
const fcRef = useRef(null)

const onClick = useCallback(() => {
setCount(count => count + 1)
fcRef.current.introduce()
}, [])

return (
<div>
点击次数: {count}
<FChild ref={fcRef} count={count}></FChild>
<button onClick={onClick}>点我</button>
</div>
)
}

自定义钩子

1
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
// 编写我们自己的hook,名字以use开头
function useCounter(initialValue) {
// 接受初始化的值生成state
const [count, changeCount] = useState(initialValue);
// 声明减少的方法
const decrease = () => {
changeCount(count - 1);
}
// 声明增加的方法
const increase = () => {
changeCount(count + 1);
}
// 声明重置计数器方法
const resetCounter = () => {
changeCount(0);
}
// 将count数字与方法返回回去
return [count, { decrease, increase, resetCounter }]
}

function RootCompent() {
// 在函数组件中使用我们自己编写的hook生成一个计数器,并拿到所有操作方法的对象
const [count, controlCount] = useCounter(10);
return (
<div>
当前数量:{count}
<button onClick={controlCount.decrease}>减少</button>
<button onClick={controlCount.increase}>增加</button>
<button onClick={controlCount.resetCounter}>重置</button>
</div>
)
}

相关链接

  1. React-Hooks
  2. 官方文档
  3. 聊聊 useCallback