快速了解React-Hooks
快速了解React-Hooks
2018-10-28

Presentational and Container Components

react团队成员之一的Dan Abramov在medium上写过一篇文章 Presentational and Container Components,他在文中将组件分为两类,分别是PresentationalContainerPresentational是展示类组件,比如说Page,Sidebar,Story,ListContainer组件是功能类组件,比如UserPage, FollowersSidebar, StoryContainer, FollowedUserList

它们是React组件的两种设计模式,和组件本身是class component还是function component关系不大。

Hooks 出现的动机

便于复用组件中的逻辑

组件中逻辑的复用,解决render props和hoc所做的事,抽离它们中的逻辑。hoc组件太多会导致标签结构混乱复杂,大多数情况下一个Container Component外面会包裹很多Presentational Component,通过Hooks,你可以从组件中抽离状态逻辑,让它们变得可以单独测试和重复使用,Hooks可以让你复用状态逻辑,而不用去变更组件的层级关系,这使得与其他组件共享Hooks变得容易,甚至可以编写社区共用的Hooks。

组件太复杂而变得难以理解

组件的生命周期中经常会混入很多不相关的逻辑,比如在componentDidMountcomponentDidUpdate中会进行数据的请求,但是在componentDidMount中我们可能还会进行一些事件的监听,在componentWillUnmount中会清理这些事件,这就使得不相关逻辑的代码混在一起,而逻辑相关的代码会被拆分在两个方法里面,这会使得维护变得困难,大多数的组件不能再次拆分成更小的组件,因为逻辑遍布整个文件。很多人会选择引入一个单独的状态管理工具,但是这又将会引入一大部分抽象,并且将在各个文件中切换。

JavaScript中的class使得学习React变得困难

为了使用class取编写组件需要编写很多无意义的代码,class组件中需要去考虑this问题,在React的开发人员中,关于class组件和function组件的讨论也存在着巨大的分歧。而且对于Prepack这种提前优化工具,class变得不那么容易去优化,React团队希望能够提供一种方式,让这些逻辑代码变得可以优化。React团队建议在设计组件的时候能够向function组件靠拢,Hooks提供了能够在function组件中去编写生命周期时的逻辑,而不需要去理解复杂的响应原理。

目前React团队没有将class组件移除的计划

React Hooks简单使用

React Hooks是React提交的一系列函数,他们提供在function组件中hook state和生命周期的特性,它们不能在class组件中被使用。

State Hook

例子:

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

function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

useState就是一个React Hooks,这个方法接收一个值,并返回一个数组,通过JavaScript的Array destructuring特性,可以将返回的值赋给变量。
useState会返回两个值,第一个是state,第二个是更新该state的方法,类似setState
一个function组件中可以使用多次useState

1
2
3
4
5
6
7
function ExampleWithManyStates() {
// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}

Effect Hook

例子:

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

function Example() {
const [count, setCount] = useState(0);

// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

React团队提出了一种叫“side effects”的操作,比如fetch data、subscriptions、或者手动去变更DOM,因为它们会影响其他组件,并且在渲染中无法进行。useEffect hook类似于类组件中componentDidMountcomponentDidUpdatecomponentWillUnmount的统一。上面的例子中将会在Effect中去改变title的值。
在使用useEffect时,只需要告诉React在更新DOM之后需要进行的“effect”,通常运行这些“effect”会在渲染之后(包括第一次渲染)。
useEffect可以通过返回一个函数来进行清理操作,这些操作会在组件unmounts时运行,或者因为后续渲染重新运行“effect”之前。下面的例子在effect中subscribe好友的状态,并且通过返回一个函数unsubscribe状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useState, useEffect } from 'react';

function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}

useState一样,在function组件中也可以多次使用useEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});

const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...

这样可以很方便的根据逻辑代码去组织逻辑,而不是通过生命周期去组织代码。

Hooks的使用规则

Hooks是JavaScript函数,但是在使用它们时需要强制遵循两条规则规则

  • 调用Hooks必须在顶层函数,不能在循环、条件和嵌套函数中调用
  • 调用Hooks必须在function组件中,不能在传统的JavaScript方法中调用(也可以在自己定义的Hooks中调用)
    React团队提供了相关的eslint插件,可以在eslint中检查上面的规则。linter plugin

自定义Hook

有时我们想在不同的组件中执行相同的逻辑,比如上面的subscribe好友状态,下面的例子中抽离了相关的逻辑,定义了一个useFriendStatus的方法

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

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});

return isOnline;
}

它接收一个friendID作为参数,并且返回好友在线的状态,现在可以在两个组件中使用它

1
2
3
4
5
6
7
8
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);

if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
1
2
3
4
5
6
7
8
9
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);

return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}

这些组件的状态是完全独立的,钩子是重用有状态逻辑的一种方式,而不是状态本身。事实上,每次调用Hook都有一个完全隔离的状态,所以你甚至可以在一个组件中使用相同的自定义Hook两次。
如果一个函数需要以use开头,在React中回叫它自定义Hooks,在lint工具中,这会是一个检查的条件。

其他Hook

有一些Hook不经常会用到,但是很有用,比如useContext,它可以让你subscribe React context

1
2
3
4
5
function Example() {
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
// ...
}

useReducer可以让你在复杂的组件中使用Reducer的方式管理state

1
2
3
function Todos() {
const [todos, dispatch] = useReducer(todosReducer);
// ...

更多的Hook可以阅读React提供的API文档 Hooks API Reference

引用

  1. Introducing Hooks