在以前的react 开发中我们习惯使用class类的写法写react组件,但是现在随着函数式编程的流行,函数作为js的第一公民,我们在react使用函数编写业务组件,在原来只能写无状态的组件,没有自身的state,hook的出现可以让函数组件钩入一些方法,可以让我们在函数组件中使用state和一些class类的组件写法,那么我们这篇笔记将以react-hook文档为基础来做一些总结。
Hook 简介 - React
State Hook
import React, { useState } from "react";
// 计算器的组件
function Main() {
const [count, setCount] = useState(0);
return (
<div>
{count}
<button
onClick={() => {
setCount(count + 1);
}}
>
增加
</button>
</div>
);
}
export default Main;
我们可以使用useState这个hook来声明state,返回是一个数组,第一个是我们的变量,第二个是设置这个变量的函数,我们使用数组解构的方式结构出来,那么在这个函数的作用域中直接使用count来展示这个state,我们调用函数直接调用setCount这个函数,这样我们就简单的使用statehook这个钩子来实现class组件this.state这个功能了,注意这个set函数和class中的setState不同,在class组件中的setState中,是要传入当前示例的全部state对象,我们往往要使用对象合并API来操作,但是hook中的set函数只需要传递当前的要设置的值即可。
setCount(100);
我们可以设置更多的state
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
我们没有必要使用这样的hook来声明多个变量,在之后的Q&A中,文档会建议我们更好的方法。
Effect Hook
在class写法中的各种生命周期的钩子,在函数组件中也有对应的hook来替代它们,那么这些钩子我们叫做effect hook,在组件渲染成功后通常会进行一些基于业务的操作,所以我们也称之为“副作用hook”,下面我们对比一下class写法和hook写法
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 (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
在componentDidMount以及componentDidUpdate这两个生命周期中,当组件被挂载或者已有prop或者state变更都会触发对应的document.title的业务逻辑,那么在class这样的写法,同样的逻辑在不同的生命写,这会让代码又臭又长,不好维护,我们使用hook来写,那么就非常简单。
import React, { useState, useEffect } from "react";
// 计算器的组件
function Main() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = "你点击了" + count + "次";
});
return (
<div>
{count}
<button
onClick={() => {
setCount(count + 1);
}}
>
增加
</button>
</div>
);
}
export default Main;
useEffect可以传递一个函数作为入参,我们写一个useEffect就相当于写了组件挂载和更新2个钩子,那么当组件销毁(清除副作用)我们可以这么写
useEffect(() => {
API.on(); // 比如当页面进入调用订阅API
return () => {
API.off(); // 销毁时再取消订阅API
}
});
useEffect会在每次挂载和更新都会执行我们传入的逻辑,如何控制它将在后面我们会有具体的例子。
那么我们使用effectHook和传统的class中的挂载和更新生命周期还是有区别的。effectHook不会阻塞浏览器的屏幕更新,它会让应用看起来更快。
我们肯定写过下面的逻辑代码:
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
// 调用A API
A();
// 调用B API
B();
// 调用C API
C();
}
不得不说,不同的业务逻辑混在一个生命去写,尽管我们有时会把不同的逻辑封装到不同函数中进行组合调用,但是代码还是难以直接去表达业务逻辑,这让维护代码的人叫苦连连,那么我们使用hook,将可以使用像stateHook一样,可以调用多个hook:
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
useEffect(() => {
API.on(); // 比如当页面进入调用订阅API
return () => {
API.off(); // 销毁时再取消订阅API
};
});
// ...
}
react会顺序执行多个effectHook
那么在官方文档中,提到了一个我也很苦恼的问题,在我进行第一次学习effectHook的时候,为什么组件更新还需要调用effect,直到文档做了一个很简单的查询好友信息的demo:
通过好友ID查询好友详情,好友详情组件需要接收一个prop,那么如果当prop进行更新,组件内的effect回调不会触发还是展示上一个好友的信息,而在清除副作用的时候,因为ID错误导致程序异常,那么显然effect设计如此是为了规避程序BUG,那么在class类中,我们应该使用如下方式来规避这样的更新问题:
componentDidUpdate(prevProps) {
// 取消订阅之前的 friend.id
API.off(prevProps.id); // 销毁时再取消订阅API
// 订阅新的 friend.id
API.on(prevProps.id);
}
而使用hook则不需要关心更新组件带来的异常, 因为使用effect时,在调用一个新effect时就会清除上一个effect(触发清除回调effect),也就不会产生因为忘记写update逻辑造成的BUG
我们既然知道了effectHook更新设计的原因,那么当部分业务逻辑因为更新而造成的多余性能浪费,class和hook解决方案是怎么样的呢?
// class的写法,比较ID是否变更,变更再进行逻辑
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
// effectHook提供了第二个可选参数,我们可以传入一个数组,比如
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
当我们传入一个空数组时
useEffect(() => {
document.title = `You clicked ${count} times`;
}, []);
那么此effect将在挂载销毁执行,任何值的update都和此effect无关,所以这样的写法一定要注意应用场景。
总结Effect Hook:
- 自带的清除机制,可以避免在class中因为update而造成的问题
- 使用effect可以让业务逻辑分离解耦,不会像class中的生命周期分离逻辑变的很难
Hook规则概要
hook本质就是JS的函数,但是使用它也需要规则,虽然有专门的语法检查工具:
eslint-plugin-react-hooks
- hook需要写在函数中最高的层级,不要在循环,嵌套中去使用它们
- 只能在React中使用hook,不要在普通JS函数中去使用hook
关于插件的使用,文档也有非常清楚的配置方法
Hook 规则 - React
至于为什么hook只能在函数中最高层级去使用,官方也在文档中说明了:
demo1:
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;
});
// ...
}
useState和useEffect如果都在最高层(函数作用域的最高层)中,那么React就可以把对应的Mary和name 进行关联,Poppins和surname进行关联,react通过声明的顺序去查找对应的变量,每次渲染的顺序和声明的顺序都是相同的,所以React才可以找到,那么如果我们写了如下的代码:
if(name !== null){
useEffect(function persistForm() {
localStorage.setItem('name ', name);
});
}
这样触犯了hook的第一条规定,导致了程序有可能不会声明useEffect这个钩子,如果没有声明,那么声明的顺序和渲染的顺序就会不一致,就导致这个钩子之后的所有声明的钩子都将会被提前执行,导致BUG产生。
自定义Hook的实现
我们自己写一个求差的小demo,参数传入减数和被减数简单体验一下自定义hook:
这个demo展示了hook和组件的传参,当组件的count改变时会触发到hook中的effect,effect会重新计算差并返回。
import { useState, useEffect } from "react";
export function useSeekingDifference(
subtraction: number,
minuend: number
): number {
const [difference, setDifference] = useState(0);
useEffect(() => {
setDifference(subtraction - minuend);
}, [subtraction, minuend]);
return difference;
}
使用hook
import React, { useState, useEffect } from "react";
// 引入自定义hook
import { useSeekingDifference } from "./../hook/computed";
// 计算器的组件
function Main() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = "你点击了" + count + "次";
});
console.log(useSeekingDifference(count * 2, count)); // 使用自定义hook
}
export default Main;
我们写自定义hook的目标就是:
- 抽离业务逻辑
但是对于业务性非常强,以及对状态管理有频繁操作的建议使用redux(尽管我没用过,但是官方建议这样)
我们书写hook时需要注意:
- hook不是react特有,只是遵循了hook的规定的函数
- 必须使用use开头,因为不仅仅代表了它是一个hook,也可以让插件去做校验规则
- 相同hook不会共享state,每一个hook都是有独立的state和effect的,是重用状态逻辑机制