一、Hook 简介
在官网的描述是这样的:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
1. 大纲
本文将主要针对开发React Hook的动机,即为什么需要它进行讲述,同样这也是React Hook的价值,并对最常用的两个Hook函数(State Hook和Effect Hook)的使用进行详细举例说明。
2. 简要概述
Hook 是一个特殊的函数,它可以让你 “钩入” React 的特性,包括state、生命周期等。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。
3. React为什么需要Hook呢
看客姥爷们如果觉得第一遍看完以下原因不好直接理解,建议可以简单过一下本节,看完后续小节后重新回顾一遍本节噢~
3.1 在组件间复用状态逻辑很难
在原本的代码中,我们经常使用高阶组件(HOC)/render props等方式来复用组件的状态逻辑,无疑增加了组件树层数及渲染,使得代码难以理解,而在 React Hook 中,这些功能都可以通过强大的自定义的 Hook 来实现,我将在后面自定义Hook小节中对其进行举例详细说明。
3.2 复杂组件变得难以理解
这里存在两种情况:
1. 同一块功能的代码逻辑被拆分在了不同的生命周期函数中
2. 一个生命周期中混杂了多个不相干的代码逻辑
二者均不利于我们维护和迭代。例如,组件常常在 componentDidMount
和 componentDidUpdate
中获取数据。还有,在 componentDidMount
中设置事件监听,而之后需在 componentWillUnmount
中清除等。同时在 componentDidMount
中也可能还有其他的功能逻辑,导致不便于理解代码。通过 React Hook 可以将功能代码聚合,方便阅读维护,我将在后面Effect Hook小节中对其进行举例说明*如何将功能分块,实现关注点分离。
3.3 其他便利好处
1. 不用再考虑该使用无状态组件(Function)还是有状态组件(Class)而烦恼,使用hook后所有组件都将是Function
在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI组件我们可以使用函数,无状态组件来展示UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给UI组件进行渲染。
2. 不用再纠结使用哪个生命周期钩子函数
3. 不需要再面对this
**
二、State Hook使用
它让我们在 React 函数组件上添加内部 state,以下是一个计数器例子:
import React, { useState } from 'react';
function Example() {
// useState 有两个返回值分别为当前 state 及更新 state 的函数
// 其中 count 和 setCount 分别与 this.state.count 和 this.setState 类似
const [count, setCount] = useState(0);// 数组解构写法
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Example;
原来类组件的等价功能是如何实现呢?是不是稍微简洁了些呢?
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
export default Example;import React, { useState } from 'react';
function Example() {
// useState 有两个返回值分别为当前 state 及更新 state 的函数
// 其中 count 和 setCount 分别与 this.state.count 和 this.setState 类似
const [count, setCount] = useState(0);// 数组解构写法
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Example;
需要注意的是 useState 不帮助你处理状态,相较于 setState 非覆盖式更新状态,useState 覆盖式更新状态,需要开发者自己处理逻辑,如下所示:
import React, { useState } from "react";
function Example() {
const [obj, setObject] = useState({
count: 0,
name: "ml"
});
return (
<div>
Count: {obj.count}
<button onClick={() => setObject({ ...obj, count: obj.count + 1 })}>+</button>
</div>
);
}
export default Example;
三、Effect Hook使用
Effect Hook 可以让你在函数组件中执行副作用操作,可以将其当作 componentDidMount
, componentDidUpdate
和 componentWillUnmount
这三个函数的组合,根据是否需要清除副作用可分为两种。
(副作用操作包括:异步请求,设置订阅以及手动更改 React 组件中的 DOM 等)
3.1 不需要清除副作用
即在更新 DOM 之后运行的一些额外的代码,包括异步请求、手动更改 DOM 等,以下为手动更改 React 组件中的 DOM 的一个例子:
import React, { useState, useEffect } from 'react';
function Example() {
// 声明一个新的叫做 "count" 的 state 变量
const [count, setCount] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate,在挂载和更新时均会执行第一个参数的函数
useEffect(() => {
document.title = `You clicked ${count} times`;// 使用浏览器的 API 更新页面标题
}, [count]);// 第二个参数作用为仅在 [count] 更改时更新,需要注意的是该参数为一个数组,只要有一个元素变化即会执行 effect
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Example;
原来类组件的等价功能如何实现呢?是不是稍微简洁了些呢?
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>
);
}
}
export default Example;
3.2 需要清除副作用
有时为了防止内存泄漏,需要清除一些副作用,以下为一个定时计数器例子,这节先看看原来类组件应该如何实现:
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
const { count } = this.state;
document.title = `You have waited ${count} seconds`;
this.timer = setInterval(() => {
this.setState(({ count }) => ({
count: count + 1
})); // 友情提醒:可以理解一下这里 setState 的用法
}, 1000);
}
componentDidUpdate() {
const { count } = this.state;
document.title = `You have waited ${count} seconds`;
}
componentWillUnmount() {
document.title = "componentWillUnmount";
clearInterval(this.timer);
}
render() {
const { count } = this.state;
return (
<div>
Count:{count}
<button onClick={() => clearInterval(this.timer)}>clear</button>
</div>
);
}
}
export default App;
以上例子需要结合多个生命周期才能完成功能,还有重复的编写,来看看 useEffect 的写法:
import React, { useState, useEffect } from "react";
let timer = null;
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You have waited ${count} seconds`;
},[count]);
useEffect(() => {
timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 返回值可类比 componentWillUnmount,返回的是清除函数
// 返回值(如果有)则在组件销毁或者调用第一个参数中的函数前调用(后者也可理解为执行当前 effect 之前对上一个对应的 effect 进行清除)
return () => {
document.title = "componentWillUnmount";
clearInterval(timer);
};
}, []); // 空数组代表第二个参数不变,即仅在挂载和卸载时执行
return (
<div>
Count: {count}
<button onClick={() => clearInterval(timer)}>clear</button>
</div>
);
}
export default App;
3.3 使用多个 Effect 实现关注点分离
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
下面是 React 官网的例子,较好的说明了将组件中不同功能的代码分块,实现关注点分离,便于维护管理。
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);
};
});
// ...
}
而同样功能在类组件中则需要较为麻烦的写法,并且不便于理解,如下所示:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
}
componentDidUpdate(prevProps) {
document.title = `You clicked ${this.state.count} times`;
// 取消订阅之前的 friend.id
ChatAPI.unsubscribeFromFriendStatus(prevProps.friend.id, this.handleStatusChange);
// 订阅新的 friend.id
ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
}
四、自定义 Hook
这里从一个简单的表单验证入手来了解自定义Hook的方法及复用状态逻辑,首先是一个自定义的姓名校验Hook
import { useState, useEffect } from 'react'
export default function useName(initialName = '') {
const [name, setName] = useState(initialName)
const [isValid, setIsValid] = useState(false)
const [message, setMessage] = useState(undefined)
useEffect(() => {
if (name.length >= 2 && name.length <= 5) {
setIsValid(true)
setMessage(undefined)
} else {
setIsValid(false)
setMessage('输入的名字不可以低于2位或超过5位')
}
}, [name])
let result = {
value: name,
isValid,
message
}
return [result, setName]
}
我们再来看看,在组件中如何使用这个自定义的Hook来完成姓名验证
import React from 'react'
import useName from './hook/useName'
export default function Example() {
let [name, setName] = useName('')
return (
<div>
<input value={name.value} onChange={(e) => {setName(e.target.value)}} />
{!name.isValid && <p>{name.message}</p>}
</div>
)
}
同样,该自定义的姓名校验Hook可以在项目其他需要姓名校验的地方使用来实现复用。
可以较为明显看出,Hook能够有效地复用状态逻辑,相较于高阶组件和Render props来实现复用状态逻辑,Hook没有嵌套过深的问题,使用起来比较简便清晰。
五、 Hook 规则
1. 只能在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook
function Form() {
const [name, setName] = useState('Mary');
useEffect(function updateTitle() {
document.title = name;
});
// 不合规则的错误方式
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
// 修正上条错误的方式
useEffect(function persistForm() {
// 如需条件判断,应将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});
}
2. 只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook,可以:
- 在 React 的函数组件中调用 Hook
- 在自定义 Hook 中调用其他 Hook
六、结语
个人认为 Hook 最大的两个亮点便是状态逻辑的复用和关注点分离的思想,同样其他写法上的优化点也是可圈可点的,大佬们怎么看呢?恳请批评指正 biubiubiu~
**
七、参考
30分钟精通React Hooks
React Hooks原理
React Hooks原理(源码解析)
精读《Hooks 取数 - swr 源码》