0%

React Hook 初探

假设大家在阅读完笔者这篇文章的前提下,有兴趣的同学可以思考下 class 组件 和我们本篇叙述的 Hook 函数组件之前有哪些区别,或者相比较 Hook 有哪些提升点呢?大家更喜欢那种代码风格呢? 欢迎评论区留言~

在这之前

我们现在想要实现一个获取表数据并可根据某一条件筛选的基本功能,分别用 class 和 hook 组件实现的伪代码如下:

class 实现伪代码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import * as React from 'react';
import { withRouter } from 'react-router'
import { Icon, Card, Input, Table, Button } from 'antd';
import Api from '../../api';
const Search = Input.Search;
const initState = {
loading: false,
data: [],
params: {
search: '',
sort: '',
filed: ''
},
pagination: {
current: 1,
total: 0,
pageSize: 15
}
}
type ProjectStates = typeof initState;
class ProjectsList extends React.Component<any, ProjectStates> {
state: ProjectStates = {
loading: false,
data: [],
params: {
search: '',
sort: '',
filed: ''
},
pagination: {
current: 1,
total: 0,
pageSize: 15
}
}
componentDidMount () {
this.getTableData();
}
handleSearch = (value: any) => {
const params = Object.assign({}, this.state.params)
params.search = value;
this.setState({
params
}, this.getTableData);
}
handleTableChange = (paginations: any, filters: any, sorter: any) => {
const params = Object.assign({}, this.state.params)
const pagination = Object.assign({}, this.state.pagination)
pagination.current = paginations.current;
if (sorter.field) {
params.filed = sorter.field;
params.sort = sorter.order === 'descend' ? 'desc' : 'asc';
} else {
params.filed = '';
params.sort = '';
}
this.setState({
params,
pagination
}, this.getTableData)
}
getTableData = async () => {
this.setState({
loading: true
})
const { pagination: { current, pageSize },
params: { search, filed, sort } } = this.state;
let res = await Api.comm.getProjectList({
currentPage: current,
pageSize,
searchName: search || undefined,
orderBy: filed ? filed.replace(/([A-Z])/g, '_$1').toLowerCase() : undefined,
sort: sort || undefined
});
this.setState({
loading: false
})
if (res && res.code == 1) {
this.setState({
data: res.data.data,
pagination: {
...this.state.pagination,
total: res.data.totalCount
}
})
}
}
render () {
const { loading, data, pagination } = this.state;
return (
<div className="projects-list">
<header className="projects-header"><Icon type="rollback" onClick={this.gotoWelcome} />项目列表</header>
<Card
noHovering
bordered={false}
title={
<Search
onSearch={this.handleSearch}
placeholder='按项目名称、项目显示名搜索'
style={{ width: 267 }} />
}
extra={<Button className='o-font--normal' type="primary" onClick={this.handleNewProject}>创建项目</Button>}>
<Table
rowKey="id"
loading={loading}
onChange={this.handleTableChange}
columns={this.initCol()}
dataSource={data}
pagination={pagination}
/>
</Card>
</div>
);
}
}
export default withRouter(ProjectsList);

Hook 实现伪代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import React, { useState, useEffect, useReducer, useRef, Fragment } from 'react';
import { withRouter } from 'react-router'
import { Icon, Card, Input, Table, Button } from 'antd';
import Api from '../../api';
import { Pagination } from 'typing'
const Search = Input.Search;
const ProjectsList: React.FC = (props: any) => {
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<any[]>([]);
const [pagination, setPagination] = useState<Pagination>({ current: 1, total: 0, pageSize: 15 });
const [params, setParams] = useState<{
search: string;
sort: string;
filed: string;
}>({ search: '', sort: '', filed: '' })
// 直接使用 effect
const getProjectList = async () => {
setLoading(true);
const { current, pageSize } = pagination;
const { search, filed, sort } = params;
let res = await Api.comm.getProjectList({
currentPage: current,
pageSize: pageSize,
searchName: search || undefined,
orderBy: filed ? filed.replace(/([A-Z])/g, '_$1').toLowerCase() : undefined,
sort: sort || undefined
});
if (res && res.code == 1) {
setData(res.data.data);
setPagination((state: Pagination) => {
return {
...state,
total: res.data.totalCount
}
})
}
setLoading(false);
}
const handleSearch = (value: any) => {
setParams(state => ({ ...state, search: value }))
}
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
const newParams = Object.assign({}, params)
if (sorter.field) {
newParams.filed = sorter.field;
newParams.sort = sorter.order === 'descend' ? 'desc' : 'asc';
} else {
newParams.filed = '';
newParams.sort = '';
}
setParams(newParams);
setPagination(state => ({ ...state, current: pagination.current }));
}
useEffect(() => {
getProjectList();
}, [params])
const [{ loading, data }] = useProjectList(params, pagination)
return (
<div className="projects-list">
<header className="projects-header"><Icon type="rollback" onClick={gotoWelcome} />项目列表</header>
<Card
noHovering
bordered={false}
title={
<Fragment>
<Search
onSearch={handleSearch}
placeholder='按项目名称、项目显示名搜索'
style={{ width: 267 }}
ref={searchInputEl}
/>
</Fragment>
}
extra={<Button className='o-font--normal' type="primary" onClick={handleNewProject}>创建项目</Button>}
>
<Table
rowKey="id"
loading={loading}
onChange={handleTableChange}
columns={initCol()}
dataSource={data}
pagination={pagination}
/>
</Card>
</div>
)
}
export default withRouter(ProjectsList);

一、Hook 概念

1. Hook 是什么?

它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,Hook 本质就是 JavaScript 函数。

2. 为什么要使用 Hook?

React 官方给出了 3 点理由,这里我们总结为2点原因:

  • 组件间复用功能(带组件内部状态的逻辑很难重用)。
  • 无 class 中的 this 和生命周期函数。

React 官方推荐我们可以渐进式地尝试去使用 hook, 因为它是 100% 向后兼容的,不包含任何破坏性改动;并且我们在我们的项目中可以 class 和 hook 混合使用。

3. 目前已存在哪些 Hook?

  • 基础 Hook

    • useState
    • useEffect
    • useContext
  • 额外的 Hook

    • useReducer (管理组件内部复杂 state)
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

二、Hook 基础使用(Hook 和 Class 区别)

我们简单介绍大家在平常编码时使用频率比较高的几个 Hook;更多 Hook 相关知识学习,官方文档 走一波。

  1. useState: 初始化 state,返回值为:当前 state 以及更新 state 的函数,它与 class 里面的 this.state 提供的功能完全相同。
1
2
3
4
5
6
7
8
<!--声明多个 state-->(官方推荐分离独立 state 变量,即将相互影响的 state 使用一个 state 变量替代)
<!--state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化-->
const [age, setAge] = useState(18);
const [address, setAddress] = useState('xihu');

const [state, setState] = useState({ age: 18, address: 'xihu' })
<!--更新 state-->
setState(state => ({...state, age: '20'}))
  1. useEffect: useEffect 会在每次 DOM 渲染后执行,不会阻塞页面渲染。它同时具备 componentDidMount、componentDidUpdate 和 componentWillUnmount 三个生命周期函数的执行时机。并且具有清楚机制。
    另外我们需要了解下副作用的概念:

副作用(网络请求、订阅某个模块或者 DOM 操作都是副作用的例子,Effect Hook 是专门用来处理副作用的
在 class 中我们通常在生命周期函数中处理副作用,Hook 中我们通过 effect 处理副作用。

1
2
3
4
useEffect(() => {
getProjectList();
<!--return function cleanUp() {}-->
}, [param]) // 仅在 param 更改时更新

useEffect 它在第一次渲染之后和每次更新之后都会执行,当我们想要跳过 Effect 进行性能优化或者约束其执行时机,这个时候需要传入第二个参数,上述代码表示 useEffect 仅在 仅在 param 更改时更新,这类比与我们的 class 中的 componentDidUpdate。
当 useEffect 的返回值是一个函数的时候,React 会在下一次执行这个副作用之前执行一遍清理工作,整个组件的生命周期流程可以这么理解:
组件挂载 –> 执行副作用 –> 组件更新 –> 执行清理函数 –> 执行副作用 –> 组件更新 –> 执行清理函数 –> 组件卸载
注意:useEffect 不允许return 任何值(清楚函数除外)

  1. useReducer: useReducer 的用法跟 Redux 非常相似,当 state 的逻辑比较复杂时,我们使用这个 Hook 比 useState 会更好。

基础使用代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const testReducer = (state, action) => {
switch (action.type) {
case 'GET_DATA':
return {
...state,
data: action.payload
}
...
...
}
};
const [state, dispatch] = useReducer(testReducer, initState);
dispatch({ type: 'GET_DATA', payload: result.data })

注意:useReducer 所维护的 state 是在当前组件内部的,这与 redux 的 reducer 组件数据共享有所区别。

  1. useRef:返回一个可变的 ref 对象,将 ref 对象的 .current 属性设置为相应的 DOM 节点或其他数据。

我们常见的用例便是用其命令式地访问子组件:

1
2
3
4
5
6
7
const searchInputEl = useRef(null)
<Search
onSearch={handleSearch}
placeholder='按项目名称、项目显示名搜索'
style={{ width: 267 }}
ref={searchInputEl}
/>
  1. React.memo: 它不是一个 Hook, 理解为一个高阶组件,等效于 PureComponent,类比与 class shouldComponentUpdate 函数,但其只比较 props,不比较 state。
    使用示例下:
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 我们自定义组件 -->
    function MyComponent(props) {
    /* 使用 props 渲染 */
    }
    <!-- 比较 props 函数 -->
    function areEqual(prevProps, nextProps) {
    }
    export default React.memo(MyComponent, areEqual);

注意:如果 props 相等,自定义比较函数 areEqual 会返回 true;如果 props 不相等,则返回 false,与shouldComponentUpdate 恰巧是相反的

  1. 自定义 Hook: Hook 必须以use开头,在里面可以调用其它的 Hook。入参和返回值都可以根据需要自定义(视情况而定),没有特殊的约定。使用也像普通的函数调用一样,Hook 里面其它的 Hook(如useEffect)会自动在合适的时候调用。

我们常用的业务逻辑自定义 Hook:

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
const useProjectList = (query, pagination) => {
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<any[]>([]);
useEffect(() => {
const getProjectList = async () => {
setLoading(true);
const { current, pageSize } = pagination;
const { search, filed, sort } = params;
let res = await Api.comm.getProjectList({
currentPage: current,
pageSize: pageSize,
searchName: search || undefined,
orderBy: filed ? filed.replace(/([A-Z])/g, '_$1').toLowerCase() : undefined,
sort: sort || undefined
});
if (res && res.code == 1) {
setData(res.data.data);
setPagination(state => ({ ...state, total: res.data.totalCount }));
}
setLoading(false);
}
getProjectList().then();
// return clearFunc;
}, [query])
return [{ loading, data }]
}

Hook 使用注意点:

  • 只在最顶层使用 Hook
  • 只在 React 函数中调用 Hook

项目中可以通过安装 Hook lint 插件 避免此类问题。

componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法。

三、Hook 使用必备条件

我们想要启用 Hook,所有 React 相关的 package 都必须升级到 16.8.0 或更高版本。(react、react-dom、react-test-renderer等)
这是笔者在升级时遇到的报错:Cannot assign to read only property ‘exports’ of object

1
install @babel/plugin-transform-modules-commonjs

nstall 之后项目正常运行。
目前 Hook 还处于早期阶段,一些第三方的库可能还暂时无法兼容 Hook,但是像 Redux、react-router 等一些流行的库已经发布了 Hook 版本。
更多 Hook 相关技术让我们一起去探索吧!

https://www.robinwieruch.de/react-hooks-fetch-data
https://react-redux.js.org/api/hooks
https://reacttraining.com/react-router/web/api/Hooks/uselocation

-------------本文结束, 感谢您的阅读-------------