React Hooks 入门

前言

React Hooks 是 React v16.8 版本中引入的新特性,阅读本文需要一定的 React 基础。

一、Hooks 出现的原因

React 的核心是组件。v16.8 版本之前,组件的标准写法是类,还有一种就是函数式组件或者(说是无状态组件)。

类组件

下面是一个简单的类组件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react';

class Demo extends Component {
state = { count: 0 }
render() {
return (
<div>
<p>点击了 { this.state.count } 次</p>
<button onClick={ this.addCount.bind(this) }>确定</button>
</div>
);
}

addCount() {
this.setState({ count: this.state.count+1 })
}
}

export default Demo;

组件类的几个缺点:

  • 大型组件很难拆分和重构,也很难测试。
  • 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  • 组件类引入了复杂的编程模式,比如 render props 和高阶组件。

无状态组件

要求必须是一个纯函数,不能包含状态,不支持生命周期方法。

React Hooks 设计目的

所以 React Hooks 的设计目的,就是加强版函数组件,完全不使用”类”,就能写出一个全功能的组件。

二、Hook 的含义

Hook 这个单词的意思是”钩子”。
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。 React Hooks 就是那些钩子。

你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。

所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 usexxx。

下面介绍 React 默认提供的四个最常用的钩子。

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

三、useState():状态钩子

useState()用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

本文前面那个组件类,用户点击按钮,会导致按钮的文字改变,文字取决于用户是否点击,这就是状态。使用useState()重写如下。

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

const Demo = () => {
const [ count, addCount ] = useState(0);

return (
<div>
<p>点击了 { count } 次</p>
<button onClick={()=>{ addCount(count+1) }}>确定</button>
</div>
)
}

export default Demo;

useState() 函数接受状态的初始值,作为参数,上例的初始值为要展示的数值0。该函数返回一个长度为2的数组,数组的第一个值是一个变量(上例是count),指向状态的当前值。第二个值是一个函数,用来更新状态。

useState 使用注意
useState 的执行顺序在每一次更新渲染时必须保持一致,否则多个 useState 调用将不会得到各自独立的状态,也会造成状态对应混乱。比如在条件判断中使用 hook,在循环,嵌套函数中使用 hook,都会造成 hook 执行顺序不一致的问题。最后导致状态的混乱。另外,所有的状态声明都应该放在函数顶部,首先声明。

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

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

现在有两个组件 Navbar 和 Messages,我们希望它们之间共享状态。

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
import React, { createContext, useContext } from "react";

// 创建一个共享状态
const UserContext = createContext({});

const Navbar = () => {
// useContext() 钩子函数用来引入 Context 对象,从中获取username属性。
const { username } = useContext(UserContext)
return (
<div>A 组件里的用户:{ username }</div>
)
}

const Messages = () => {
// useContext() 钩子函数用来引入 Context 对象,从中获取username属性。
const { username } = useContext(UserContext)
return (
<div>B 组件里的用户:{ username }</div>
)
}

function App() {
return (
// UserContext.Provider提供了一个 Context 状态共享对象,这个对象可以被子组件共享。
<UserContext.Provider value={{ username: 'PAN~~~' }}>
<div>
<Navbar />
<Messages />
</div>
</UserContext.Provider>
);
}

export default App;

五、useReducer():action 钩子

React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux。

Redux 的核心概念是,组件发出 action 与状态管理器通信(store)。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,然后再返回给状态 管理通信器,Reducer 函数的形式是(state, action) => newState。

useReducers()钩子用来引入 Reducer 功能。

1
const [state, dispatch] = useReducer(reducer, initialState);

useReducer() 接收 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数。

下面看一个简单的计数器效果:

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
import React, { useReducer } from "react";

// 用于计算状态的 Reducer 函数
const myReducer = (state, action) => {
switch(action.type) {
case('countUp'):
return {
...state,
count: state.count + 1
}
default:
return state
}
}

function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 })

return (
<div className="App">
<button onClick={() => dispatch({ type: 'countUp' })}>
+1
</button>
<p>Count: {state.count}</p>
</div>
);
}

export default App;

六、useEffect():副作用钩子

useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。以前,放在componentDidMount里面的代码,现在可以放在useEffect()。具体用法如下:

1
2
3
useEffect(()  =>  {
// Async Action
}, [dependencies])

上面用法中,useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。

注意
第二个参数可以省略,这时每次组件渲染时,就会执行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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { useState, useEffect } from "react";

const Person = ({ personId }) => {
const [loading, setLoading] = useState(true);
const [person, setPerson] = useState({});

useEffect(() => {
setLoading(true);
fetch(`https://easy-mock.com/mock/5d53bf7452cb451e4e23fd53/study/getPerson?id=${personId}`)
.then(response => response.json())
.then(data => {
console.log('First Loading...')
setPerson(data.data);
setLoading(false);
});
}, [personId]);
// 每当组件参数personId发生变化,useEffect()就会执行。
// 组件第一次渲染时,useEffect()也会执行。

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

return (
<div>
<p>姓名: {person.name}</p>
<p>年龄: {person.age}</p>
</div>
);
};

function App() {
const [show, setShow] = useState("1");

return (
<div className="App">
<Person personId={show} />
<div>
<button onClick={() => setShow("1")}>Person 1</button>
<button onClick={() => setShow("2")}>Person 2</button>
</div>
</div>
);
}

export default App;

七、创建自己的 Hooks

下面的示例以上面的例子为基础,自定义一个 Hooks,usePerson()就是一个自定义的 Hook。

Person 组件就改用这个新的钩子,引入封装的逻辑。

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
import React, { useState, useEffect } from "react";

const usePerson = personId => {
const [loading, setLoading] = useState(true);
const [person, setPerson] = useState({});
useEffect(() => {
setLoading(true);
fetch(`https://easy-mock.com/mock/5d53bf7452cb451e4e23fd53/study/getPerson?id=${personId}`)
.then(response => response.json())
.then(data => {
setPerson(data.data);
setLoading(false);
});
}, [personId]);
// 返回
return [loading, person];
};

const Person = ({ personId }) => {
const [loading, person] = usePerson(personId);

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

return (
<div>
<p>姓名: {person.name}</p>
<p>年龄: {person.age}</p>
</div>
);
};

function App() {
const [show, setShow] = useState("1");

return (
<div className="App">
<Person personId={show} />
<div>
<button onClick={() => setShow("1")}>Person 1</button>
<button onClick={() => setShow("2")}>Person 2</button>
</div>
</div>
);
}

export default App;

(完)