0%

再探 React Hooks

在之前的一章我们已经了解了 React Hook 的一些基本特性/优点和一些使用方法。在这一章中,我们将深入介绍另外几个常用的钩子。

React Hooks Intro

一、类与函数的区别

React的核心是组件。在v16.8版本之前,组件的标准写法是类组件。

1.1 类组件的缺点

  • 组件类代码很“重”
  • 真实的 React App 由多个类按层级,一层层构成,复杂度很高
  • 如果再使用 Redux,整个项目看起来会变得非常复杂

根据 Redux 的作者 Dan 的总结,组件类有以下几个缺点:

  • Huge components that are hard to refactor and test.
  • Duplicated logic between different components and lifecycle methods.
  • Complex patterns like render props and higher-order components.

翻译一下就是:

  • 大型组件很难拆分、重构和测试
  • 业务逻辑存在于不同组件以及生命周期方法中,因此会导致重复逻辑
  • 复杂的编程模式,如render props 和高阶组件

1.2 函数组件的优势

React 官方希望组件是一个简单的数据流管道,让开发者可以根据需求来组合管道,而不要变成一个复杂的容器。因此组件的最佳写法是函数式。

早期的函数组件无法取代类组件,是因为函数组件必须是纯函数,因此不能包含状态生命周期方法

而 React Hook 的出现彻底解决了这个问题,它可以让我们用函数式组件的写法,完成一个功能齐全的组件。

二、What is Hook?

React Hooks 是为函数组件提供所需要的外部功能副作用的“钩子”。

因此我们需要什么功能,就使用什么钩子。

要注意的是,如果我们自己封装自定义的钩子,需要遵循命名规则,即钩子一律使用 use 前缀来命名,以便于识别。

接下来,我们来介绍几个 React 提供的常用的钩子。

  • useState()
  • useContext()
  • useReducer()
  • useEffect()

三、useState(): 状态钩子

由于纯函数不能有状态,所以需要把状态放在钩子里面。

useState() 函数接受状态的初始值作为参数,并返回一个数组,数组的第一个值为表示当前状态值的变量,第二个值为用来更新状态的函数(命名约定为:以set前缀+状态的变量名)。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { useState } from 'react';

export default function Button() {
const [buttonText, setButtonText] = useState('Hello!');

const handleClick = () => {
setButtonText('World!');
};

return (
<button onClick={handleClick}>{buttonText}</button>
);
}

四、useContext(): 共享状态钩子

如果需要在组件间共享状态,可以使用useContext()

例如组件A和组件B需要共享状态,示例如下:

1
2
3
4
5
6
7
8
9
// App.jsx
const AppContext = React.createContext({});

<AppContext.Provider value = {{username: 'Will'}}>
<div className='App'>
<A />
<B />
</div>
</AppContext.Provider>
1
2
3
4
5
6
7
8
9
10
// A.jsx
export default function A() {
const { username } = useContext(AppContext);
return (
<div className='A'>
<p>Home</p>
<p>{username}</p>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
// B.jsx
export default function B() {
const { username } = useContext(AppContext);

return (
<div className='B'>
<h1>B Component</h1>
<p>the current user is: {username}</p>
</div>
);
}

五、useReducer(): action 钩子

React 本身并不提供状态管理工具,如果需要,通常是引入外部库,当前最流行的库非 Redux 莫属。

Redux 的核心理念是:组件发出 action 与状态管理器通信,在状态管理器收到 action 后,使用 Reducer 函数计算出最新的状态并返回其值(Reducer函数形式大致为:(state, action) => newState)。(具体关于 Redux 的介绍,请参考我之前的关于 Redux 的文章)

随着 Redux 的流行,React 官方也加入了 useReducers() 这个钩子用来引入 Reducer 功能,从而可以更方便的使用 Redux。

**useReducer()**的基本用法:接受 Reducer 函数和状态初始值作为参数,并返回一个数组。数组的第一个值是当前状态值,第二个值是用来发送 action 的 dispatch 函数。

以计数器为例,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建 reducer 函数
const myReducer = (state, action) => {
switch(action.type) {
case('countUp'):
return {
...state,
count: state.count + 1
}
case ('countDown'):
return {
...state,
count: state.count - 1
}
default:
return state;
}
}
1
2
3
4
5
6
7
8
9
10
11
// 计算器组件
function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 })
return (
<div className='App'>
<button onClick={() => dispatch({type: 'countUp'})}>+1</button>
<button onClick={() => dispatch({type: 'countDown'})}>-1</button>
<p>Count: {state.count}</p>
</div>
)
}

尽管使用 Hook 可以提供共享状态以及Reducer 函数,但是由于 React 本身并没有提供中间件(middleware)以及时间旅行(time travel),因此,如果需要使用这两个功能,还是要用 Redux。

六、useEffect(): 通用副作用钩子

这个在上一章中已经详细讲解,这里不多赘述。只给出一个简单示例,如下:

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
// Person 组件
export default function Person({ personId }) {
const [loading, setLoading] = useState(true);
const [person, setPerson] = useState({});

useEffect(() => {
setLoading(true);
fetch(`https://getApi/${personId}`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(false);
});
}, [personId])

if (loading === true) {
return <p>Loading...</p>
}

return (
<div>
<p>Name: {person.name}</p>
<p>Age: {person.age}</p>
</div>
);
}

七、创建自定义的 Hook

如上面代码示例所示,我们也可以将其封装起来以变成一个自定义的 Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 封装为 Hook
function usePerson(personId) {
const [loading, setLoading] = useState(true);
const [person, setPerson] = useState({});

useEffect(() => {
setLoading(true);
fetch(`https://getApi/${personId}`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(true);
})
}, [personId])

return [loading, person];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Person 组件
export default function Person({ personId }) {
const [loading, person] = usePerson(personId);

if (loading === true) {
return <p>Loading...</p>;
}

return (
<div>
<p>Name: {person.name}</p>
<p>Age: {person.age}</p>
</div>
);
}

Notes:以上内容借鉴了阮一峰老师的文章。