Hooks是React 16.8的新特性。这些特性让你不必专门写class组件,来使用state和其他React特性。
构建自己的自定义Hooks让你能从组件中提取复用的逻辑函数。
在我们学习Effect Hook的时候,我们见过这个聊天APP中的组件:展示一个好友在线状态的列表:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
现在我们要说的是,我们的聊天APP中还有一个联系人列表,同时我们想把那些在线的好友渲染成绿色。我们可以拷贝同样的逻辑到我们的FriendListItem组件中,但是这不是理想的方案:
import React, { useState, useEffect } from 'react';
function FriendListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
取而代之的是,我们要在这两个组件间共享这部分逻辑。
过去的React编程中,我们已经拥有两种流行的方法来共享有状态的逻辑:高阶组件和render props(render props大致思想是,在父组件中给一个名为render的props属性传递一个函数,该函数是可复用的组件或者逻辑,在自组件中调用this.props.render)。我们来看看用Hook怎么解决类似问题,同时不需要在组件树中强行的添加更多的组件。
提取自定义组件
当我们想使用js函数来共享逻辑,我们把逻辑放置在另一个函数中。组件和hooks都是函数,所以对于他们也是一样的。
自定义Hooks就是使用“use”作为命名前缀而被其他Hooks调用的JavaScript函数。比方说,上面的useFriendStatus就是我们的第一个自定义Hook:
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
这里面没有什么新知识——逻辑和先前的组件一样。也像在组件中一样,保证在只在顶层调用其他Hooks并且不要有判断条件的调用。
和React组件不一样,自定义Hook没必要有一个特别的“签名”。我们可以决定参数接受和返回值是什么样子。换句话说,就和普通函数差不多。它的名字总是起始于“use”,这样就能一样看出Hooks的规则适用于它。
useFriendsStatus的目的就是要订阅friends的状态。这就是为什么他接受friendsID作为参数,返回friends是否在线的状态:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
return isOnline;
}
使用自定义Hook
在最初,我们的目的是移除FriendStatus和FriendListItem间的重复的逻辑。这两个组件都想知道朋友的在线状态。
现在我们已经把这部分逻辑提取到useFriendStatus的Hook中,那么我们可以这样使用:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
这些代码和最初的例子等价吗?是的,他们的工作方式完全等同。如果你细看,你会注意到我们没有对这些行为作出任何改变。我们做的就是把两个函数的共有代码提取到另一个独立的函数中。自定义Hooks是遵循Hooks设计规则的自然衍生惯例,并不是一种React特性。
必须要命名自己的Hook为useXXX吗?请这么做。这个惯例很重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了Hook规则。
两个组件使用了相同的Hook会共享状态吗?不,自定义Hooks是复用逻辑的机制(就像设置订阅或者记录当前值),但每一次使用自定义Hook的时候,它们内部所有的状态和作用都是隔离的。
自定义Hook是怎么隔离状态的?每一次我们调用Hook都会得到被隔离的状态。因为我们在直接调用useFriendStatus,从React的角度看,我们的组件还是在调用useState和useEffect而已。如同我们前些章节中所学,我们在同一个组件中调用useState和useEffect多次都是完全独立的。
提示:在Hooks间传递信息
既然Hooks是函数,我们就能在它们之间传递信息。
为了说明这一点,我们使用从我们假设的APP中的另一个组件。这是一个聊天对象选择器,她会显示当前选择的好友是否在线:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
我们保持当前选择的friend ID在状态变量recipientID中,并且在用户用select选择另一个friend的时候更新它。
因为useState Hook的使用可以给我们提供最新的recipientID的状态值,我们能把它传递给我们的自定义useFriendStatus中:
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
如此可以让我们知道当前选中的好友是否在线。当我们选择不同的好友并更新 recipientID 状态变量时,useFriendStatusHook 将会取消订阅之前选中的好友,并订阅新选中的好友状态。
use你的想象
自定义Hooks提供了曾经React组件不曾拥有的共享逻辑的灵活性。你可以使用自定义Hooks来覆盖很多场景:处理、动画、订阅、定时器以及可能我们都不曾考虑的许许多多。更重要的是,我们能构建如同React原生特性一般简单的Hooks。
尽量去抵制那种过早就抽象的思维。函数组件现在能做的更多,那么你的代码库中函数组件的代码会变多。这正常——不要觉得你需要立即将组件拆分成Hooks。但是我们依旧鼓励你去发现那些可用Hooks隐藏复杂逻辑来提供简单接口的场景,或者解耦混乱的组件。
举例,也许你有一个复杂的组件,它包含了很多本地状态,这些状态已经被复杂的方式管理。useState不会集中管理你的更新逻辑,所以你可能小写一个类似Redux的reduce来:
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, {
text: action.text,
completed: false
}];
// ... other actions ...
default:
return state;
}
}
Reducer非常方便独立测试,且对于传递复杂更新逻辑也更易于扩展。如果以后有必要,你可以把他们打散成更小的reducer。但是,你还想使用本地状态,或者不想安装新的第三方库。
所以为什么我们不写一个“useReducer”的Hook,来让我们能用reducer来管理本地状态?简单的版本可能是这样的:
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
现在,我们在组件中使用它,让 reducer 驱动它管理 state:
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: 'add', text });
}
// ...
}
这种需要reducer子复杂组件中管理本地需求的场景很常见,所以我们将useReducer的Hook直接放到React中了。在下一讲API引用中,你将会发现它,以及其他内建的的Hooks。