Sebastian Markbåge 想到了 Hook 最初的设计,后来经过 Andrew Clark,Sophie Alpert,Dominic Gannaway,和 React 团队的其它成员的提炼。
React官方文档《Hook 简介》一文中有关于动机的介绍,下面的内容便是基于此对相关内容以示例的形式做了更详细的补充,以更方便读者的理解。
在组件之间复用状态逻辑很难
这里需要特别说明是,自定义Hook复用的是状态逻辑,并不是状态值。什么意思呢?话有点绕,我们来看的例子就清晰了。
Sandbox地址:https://codesandbox.io/s/zealous-mirzakhani-zcozd?file=/src/App.js
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
return (<div className="parent">
<Increase />
<Decrease />
</div>)
}
function useCountState(type) {
const [count, useCount] = useState(0);
useEffect(() => {
const time = (type === 'increase') ? 0 : 3000;
setTimeout(() => {
useCount(count + 1);
}, time);
}, []);
return count;
}
function Increase() {
const count = useCountState('increase');
return <p className="increase">
<span className="text">Increase结果:{count}</span>
</p>;
}
function Decrease() {
const count = useCountState();
return <p className="decrease">
<span className="text">Decrease结果:{count}</span>
</p>;
}
ReactDOM.render(<Parent />, mountNode);
上面示例组件 Increase 和 Decrease 复用了自定义 Hook, useCountState。’increase’ 的延迟为0,decrease 延迟3秒。直接后,我们会先看到“Increase 结果”很快变为1,“Decrease 结果”3秒后变为1。说明这里的两个状态值是不共享的,只是这段逻辑共享了。这个其实很好理解,因为自定义Hook就是一个函数,跟其他任何函数一样,没有什么特殊的。
复杂组件变得难以理解
这里主要讲了两个小点:一是相关逻辑因为要写在不同的生命周期中而被割裂,最常见的是被写在componentDidMount、componentDidUpdate 和 componentWillUnmont 中的逻辑。二是使用 reducer 对状态进行管理。
对于第二点,笔者不是很认同,笔者认为我们使用状态管理库最主要的原因是因为跨组件的状态共享,而非因 reducer 管理状态更为方便,当然使用 reducer 是一个附加值。
对于第一点使用 useEffect,确实带来一些便利,逻辑也更合理。下面以常见的单击空白区域收集菜单为例:
Sandbox地址:https://codesandbox.io/s/distracted-taussig-uggsw?file=/src/App.js
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function DownDropMenus() {
const [show, setShow] = useState(false);
const handleClick = () => {
setShow(!show);
}
const handleClickSpace = (e) => {
if (e.target !== this.dom) {
setShow(false);
}
}
useEffect(() => {
document.addEventListener('click', handleClickSpace);
return () => {
document.removeEventListener('click', handleClickSpace);
}
}, [show]);
const display = (show) ? 'block' : 'none';
return <div className="down-drop-menus">
<span className="text" onClick={handleClick} ref={(dom) => { this.dom = dom; }}>下拉菜单</span>
<ul className="menus" style={{ display }}>
<li>菜单项一</li>
<li>菜单项二</li>
<li>菜单项三</li>
</ul>
</div>
}
ReactDOM.render(<DownDropMenus />, mountNode);
相应的 CSS:
.down-drop-menus {
position: relative;
.text {
cursor: pointer;
}
.menus {
position: absolute;
top: 24px;
list-style: none;
margin: 0;
padding: 8px 0;
border: 1px solid #ddd;
li {
line-height: 28px;
padding: 0 16px;
&:hover {
background-color: #f5f5f5;
cursor: pointer;
}
}
}
}
难以理解的Class
在介绍这项的时候,有一句“class 是学习 React 的一大屏障”,虽然我不知道这个屏障在哪里。不过我还是从这段文里读到了两个重点。一是理解 this,在 React class 对象方法里 this 需要通过 bind 或者特定写法来保证指向组件本身;二是区分函数组件和 class 组件的适用场景,所以 Hook 应该是对喜欢使用函数组件用户的福音(而我偏偏是个class组件的偏好者)。使用 Hook 几乎可以适应在所有原来需要使用 class 组件的场景。
另外官网提到了使用 Prepack 来试验 component folding 时,发现使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。因为类必须有一个构造函数,相同功能的组件使用 class 组件编译只会很可能要比函数组件多一些代码,所以函数组件的文件会更小巧写。
适用范围
笔者认为 class 组件和支持 Hook 的函数组件,这两者的本质差异不大。Hook 比 class 写起来更简便,省略了 class 的那些概念与特性。也因为函数的特性,不好管理太大的组件,所以会在无形中促使自己将组件拆得更小(这一点是鼓励的)。结合上面三项和本节所属内容,官网是推荐、鼓励使用 Hook 来实现的,也适用于绝大部分场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法。
事实上目前 Hook 也不支持 getDerivedStateFromProps,但因为这个方法在功能上可以使用 componentDidMount / componentDidUpdate 来替代,所以不影响功能的实现。但还是有影响的,当 props 中的某个属性必须同 state 中的某个属性必须保持一致时,需要多执行一次。请看示例:
Sandbox地址:https://codesandbox.io/s/staging-butterfly-yn00p?file=/src/App.js
import React, { useState, useEffect, memo } from 'react';
import ReactDOM from 'react-dom';
import { Button, Modal } from 'antd';
function TestDialog({ onVisibleChange, visible: pVisible }) {
const [visible, setVisible] = useState(false);
// 被执行两次
console.log(pVisible, visible);
const handleHide = () => {
setVisible(false);
if (typeof onVisibleChange === 'function') {
onVisibleChange(false);
}
}
useEffect(() => {
setVisible(pVisible);
}, [pVisible]);
return <span>
<Modal visible={visible} title="test" onClose={handleHide} onCancel={handleHide} onOk={handleHide}>Test</Modal>
</span>
}
function Parent() {
const [visible, setVisible] = useState(false);
const handleClick = () => {
setVisible(true);
}
const handleVisibleChange = (visible) => {
setVisible(visible);
}
return (<div>
<Button type="primary" onClick={handleClick}>Open</Button>
<TestDialog visible={visible} onVisibleChange={handleVisibleChange} />
</div>)
}
ReactDOM.render(<Parent />, mountNode);
每次点击“ Open ”按钮,或者“关闭”对话框,第8行的 console 都会输出两次。
如果使用 class 组件中的 getDerivedStateFromProps,就可以避免以上情况,请看第28行代码的输出。如代码所示:
Sandbox地址:https://codesandbox.io/s/modest-bird-bsedo?file=/src/App.js
import React, { Component, useState } from 'react';
import ReactDOM from 'react-dom';
import { Button, Modal } from 'antd';
class TestDialog extends Component {
state = {
visible: false
}
static getDerivedStateFromProps(props, state) {
if (props.visible !== undefined && props.visible !== state.visible) {
return { visible: props.visible };
}
return null;
}
handleHide = () => {
const { onVisibleChange } = this.props;
this.setState({ visible: false });
if (typeof onVisibleChange === 'function') {
onVisibleChange(false);
}
}
render() {
const { visible } = this.state;
// 只执行一次
console.log(visible, this.props.visible);
return (<span>
<Modal visible={visible} title="test" onClose={this.handleHide} onCancel={this.handleHide} onOk={this.handleHide}>Test</Modal>
</span>)
}
}
function Parent() {
const [visible, setVisible] = useState(false);
const handleClick = () => {
setVisible(true);
}
const handleVisibleChange = (visible) => {
setVisible(visible);
}
return (<div>
<Button type="primary" onClick={handleClick}>Open</Button>
<TestDialog visible={visible} onVisibleChange={handleVisibleChange} />
</div>)
}
ReactDOM.render(<Parent />, mountNode);
以上是笔者对 Hook 动机的一些自己的理解——如有不当之处,欢迎批评指正,谢谢!