1. Hook 简介

Hook 是 React 16.8.0 版本增加的新特性,可以在函数组件中使用 state 以及其他的 React 特性。

✌️Hook 使用规则:

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 自定义 Hook

下面介绍几个常用的 Hook。

2. useState

useState让函数组件也可以有state状态,并进行状态数据的读写操作。

1
const [xxx, setXxx] = useState(initValue); // 解构赋值

📐useState() 方法

参数

第一次初始化指定的值在内部作缓存。可以按照需要使用数字字符串对其进行赋值,而不一定是对象

如果想要在state中存储两个不同的变量,只需调用 useState() 两次即可。

返回值

包含 2 个元素的数组,第 1 个为内部当前状态值,第 2 个为更新状态值的函数,一般直接采用解构赋值获取。

📐setXxx() 的写法

setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值

setXxx(value => newValue):参数为函数接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值。

📐 完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const App = () => {
const [count, setCount] = useState(0);

const add = () => {
// 第一种写法
// setCount(count + 1);
// 第二种写法
setCount(count => count + 1);
};

return (
<Fragment>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
</Fragment>
);
};

声明了一个叫 count 的 state 变量,然后把它设为 0。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount 来更新当前的 count

在函数中,我们可以直接用 count

1
<h2>当前求和为:{count}</h2>

更新state

1
2
setCount(count + 1);
setCount(count => count + 1);

📐 使用多个 state 变量

1
2
3
4
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

📌不是必须要使用多个state变量,仍然可以将相关数据分为一组。但是,不像 class 中的 this.setStateuseState中更新state变量是替换。不是合并

3. useEffect

useEffect可以在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)。

React 中的副作用操作

  • ajax 请求数据获取
  • 设置订阅 / 启动定时器
  • 手动更改真实 DOM

📐 使用规则

1
2
3
4
5
6
7
8
9
10
11
useEffect(() => {
// 在此可以执行任何带副作用操作
// 相当于componentDidMount()
return () => {
// 在组件卸载前执行
// 在此做一些收尾工作, 比如清除定时器/取消订阅等
// 相当于componentWillUnmount()
};
}, [stateValue]); // 监听stateValue
// 如果省略数组,则检测所有的状态,状态有更新就又调用一次回调函数
// 如果指定的是[], 回调函数只会在第一次render()后执行一次

可以把 useEffect 看做如下三个函数的组合:

  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

📐 每次更新的时候都运行 Effect

1
2
3
4
5
6
7
8
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

调用一个新的 effect 之前会对前一个 effect 进行清理。下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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

📐 通过跳过 Effect 进行性能优化

如果某些特定值在两次重渲染之间没有发生变化,可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect第二个可选参数即可:

1
2
3
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [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 同样适用:

1
2
3
4
5
6
7
8
9
10
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 实现关注点分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
};
});
// ...
}

📐 完整示例

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
import React, { useState, Fragment, useEffect } from 'react';
import ReactDOM from 'react-dom';

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

useEffect(() => {
let timer = setInterval(() => {
setCount(count => count + 1);
}, 500);
console.log('@@@@');
return () => {
clearInterval(timer);
};
}, [count]);
// 检测count的变化,每次变化,都会输出'@@@@'
// 如果是[],则只会输出一次'@@@@'

const add = () => {
setCount(count => count + 1);
};

const unmount = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
};

return (
<Fragment>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
<button onClick={unmount}>卸载组件</button>
</Fragment>
);
};

export default App;

4. useRef

useRef可以在函数组件中存储 / 查找组件内的标签或任意其它数据。保存标签对象,功能与 React.createRef() 一样。

1
const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

📌 当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Fragment, useRef } from 'react';

const Demo = () => {
const myRef = useRef();

//提示输入的回调
function show() {
console.log(myRef.current.value);
}

return (
<Fragment>
<input type="text" ref={myRef} />
<button onClick={show}>点击显示值</button>
</Fragment>
);
};

export default Demo;

5. Hook 规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook ,在 React 函数的最顶层调用 Hook。

如果想要有条件地执行一个 effect,可以将判断放到 Hook 的内部

1
2
3
4
5
6
useEffect(function persistForm() {
// 👍 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});

只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook。可以:

  • 在 React 的函数组件中调用 Hook
  • 在自定义 Hook 中调用其他 Hook