一、前言
- React Hooks 是从 v16.8 引入的又一开创性的新特性。第一次了解这项特性的时候,真的有一种豁然开朗,发现新大陆的感觉。我深深的为 React 团队天马行空的创造力和精益求精的钻研精神所折服。本文除了介绍具体的用法外,还会分析背后的逻辑和使用时候的注意事项,力求做到知其然也知其所以然。
二、Hooks 的由来
Hooks的出现是为了解决 React 长久以来存在的一些问题:
- 带组件状态的逻辑很难重用
为了解决这个问题,需要引入render props或higher-order components这样的设计模式,如react-redux提供的connect方法。这种方案不够直观,而且需要改变组件的层级结构,极端情况下会有多个wrapper嵌套调用的情况。
Hooks可以在不改变组件层级关系的前提下,方便的重用带状态的逻辑。
- 复杂组件难于理解
大量的业务逻辑需要放在componentDidMount和componentDidUpdate等生命周期函数中,而且往往一个生命周期函数中会包含多个不相关的业务逻辑,如日志记录和数据请求会同时放在componentDidMount中。另一方面,相关的业务逻辑也有可能会放在不同的生命周期函数中,如组件挂载的时候订阅事件,卸载的时候取消订阅,就需要同时在componentDidMount和componentWillUnmount中写相关逻辑。
Hooks可以封装相关联的业务逻辑,让代码结构更加清晰。
- 难于理解的 Class 组件
JS 中的this关键字让不少人吃过苦头,它的取值与其它面向对象语言都不一样,是在运行时决定的。为了解决这一痛点,才会有剪头函数的this绑定特性。另外 React 中还有Class Component和Function Component的概念,什么时候应该用什么组件也是一件纠结的事情。代码优化方面,对Class Component进行预编译和压缩会比普通函数困难得多,而且还容易出问题。
Hooks可以在不引入 Class 的前提下,使用 React 的各种特性。
三、什么是 Hooks
- 上面是官方解释。从中可以看出 Hooks 是函数,有多个种类,每个 Hook 都为Function Component提供使用 React 状态和生命周期特性的通道。Hooks 不能在Class Component中使用
四、什么是 Hooks
- 先来看一个传统的Class Component:
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>
);
}
}
使用 State Hook 来改写会是这个样子:
import React, { useState } from 'react';
function Example() {
// 定义一个 State 变量,变量值可以通过 setCount 来改变
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
可以看到useState的入参只有一个,就是 state 的初始值。这个初始值可以是一个数字、字符串或对象,甚至可以是一个函数。当入参是一个函数的时候,这个函数只会在这个组件初始渲染的时候执行:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
useState的返回值是一个数组,数组的第一个元素是 state 当前的值,第二个元素是改变 state 的方法。这两个变量的命名不需要遵守什么约定,可以自由发挥。要注意的是如果 state 是一个对象,setState 的时候不会像Class Component的 setState 那样自动合并对象。要达到这种效果,可以这么做:
setState(prevState => {
// Object.assign 也可以
return {...prevState, ...updatedValues};
});
从上面的代码可以看出,setState 的参数除了数字、字符串或对象,还可以是函数。当需要根据之前的状态来计算出当前状态值的时候,就需要传入函数了,这跟Class Component的 setState 有点像。
另外一个跟Class Component的 setState 很像的一点是,当新传入的值跟之前的值一样时(使用Object.is比较),不会触发更新。
五、Effect Hook
解释这个 Hook 之前先理解下什么是副作用。网络请求、订阅某个模块或者 DOM 操作都是副作用的例子,Effect Hook 是专门用来处理副作用的。正常情况下,在Function Component的函数体中,是不建议写副作用代码的,否则容易出 bug。
下面的Class Component例子中,副作用代码写在了componentDidMount和componentDidUpdate中:
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中的代码是一样的。而使用 Effect Hook 来改写就不会有这个问题:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect会在每次 DOM 渲染后执行,不会阻塞页面渲染。它同时具备componentDidMount、componentDidUpdate和componentWillUnmount三个生命周期函数的执行时机。
此外还有一些副作用需要组件卸载的时候做一些额外的清理工作的,例如订阅某个功能:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
在componentDidMount订阅后,需要在componentWillUnmount取消订阅。使用 Effect Hook 来改写会是这个样子:
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);
// 返回一个函数来进行额外的清理工作:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
当useEffect的返回值是一个函数的时候,React 会在下一次执行这个副作用之前执行一遍清理工作,整个组件的生命周期流程可以这么理解:
组件挂载 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 组件卸载
上文提到useEffect会在每次渲染后执行,但有的情况下我们希望只有在 state 或 props 改变的情况下才执行。如果是Class Component,我们会这么做
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
使用 Hook 的时候,我们只需要传入第二个参数:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改变的时候才执行 Effect
第二个参数是一个数组,可以传多个值,一般会将 Effect 用到的所有 props 和 state 都传进去。
当副作用只需要在组件挂载的时候和卸载的时候执行,第二个参数可以传一个空数组[],实现的效果有点类似componentDidMount和componentWillUnmount的组合
作为学习传播 讲解透彻:segmentfault.com/a/119000001…