原文: Buiding Your Own Hooks

Hooks是React 16.8的新特性。这些特性让你不必专门写class组件,来使用state和其他React特性。

构建自己的自定义Hooks让你能从组件中提取复用的逻辑函数。
在我们学习Effect Hook的时候,我们见过这个聊天APP中的组件:展示一个好友在线状态的列表:

  1. import React, { useState, useEffect } from 'react';
  2. function FriendStatus(props) {
  3. const [isOnline, setIsOnline] = useState(null);
  4. useEffect(() => {
  5. function handleStatusChange(status) {
  6. setIsOnline(status.isOnline);
  7. }
  8. ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  9. return () => {
  10. ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  11. };
  12. });
  13. if (isOnline === null) {
  14. return 'Loading...';
  15. }
  16. return isOnline ? 'Online' : 'Offline';
  17. }

现在我们要说的是,我们的聊天APP中还有一个联系人列表,同时我们想把那些在线的好友渲染成绿色。我们可以拷贝同样的逻辑到我们的FriendListItem组件中,但是这不是理想的方案:

  1. import React, { useState, useEffect } from 'react';
  2. function FriendListItem(props) {
  3. const [isOnline, setIsOnline] = useState(null);
  4. useEffect(() => {
  5. function handleStatusChange(status) {
  6. setIsOnline(status.isOnline);
  7. }
  8. ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  9. return () => {
  10. ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  11. };
  12. });
  13. return (
  14. <li style={{ color: isOnline ? 'green' : 'black' }}>
  15. {props.friend.name}
  16. </li>
  17. );
  18. }

取而代之的是,我们要在这两个组件间共享这部分逻辑。
过去的React编程中,我们已经拥有两种流行的方法来共享有状态的逻辑:高阶组件和render props(render props大致思想是,在父组件中给一个名为render的props属性传递一个函数,该函数是可复用的组件或者逻辑,在自组件中调用this.props.render)。我们来看看用Hook怎么解决类似问题,同时不需要在组件树中强行的添加更多的组件。

提取自定义组件

当我们想使用js函数来共享逻辑,我们把逻辑放置在另一个函数中。组件和hooks都是函数,所以对于他们也是一样的。
自定义Hooks就是使用“use”作为命名前缀而被其他Hooks调用的JavaScript函数。比方说,上面的useFriendStatus就是我们的第一个自定义Hook:

  1. import React, { useState, useEffect } from 'react';
  2. function useFriendStatus(friendID) {
  3. const [isOnline, setIsOnline] = useState(null);
  4. useEffect(() => {
  5. function handleStatusChange(status) {
  6. setIsOnline(status.isOnline);
  7. }
  8. ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
  9. return () => {
  10. ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
  11. };
  12. });
  13. return isOnline;
  14. }

这里面没有什么新知识——逻辑和先前的组件一样。也像在组件中一样,保证在只在顶层调用其他Hooks并且不要有判断条件的调用。
和React组件不一样,自定义Hook没必要有一个特别的“签名”。我们可以决定参数接受和返回值是什么样子。换句话说,就和普通函数差不多。它的名字总是起始于“use”,这样就能一样看出Hooks的规则适用于它。
useFriendsStatus的目的就是要订阅friends的状态。这就是为什么他接受friendsID作为参数,返回friends是否在线的状态:

  1. function useFriendStatus(friendID) {
  2. const [isOnline, setIsOnline] = useState(null);
  3. // ...
  4. return isOnline;
  5. }

现在让我们看看怎么适用我们的自定义Hook吧。

使用自定义Hook

在最初,我们的目的是移除FriendStatus和FriendListItem间的重复的逻辑。这两个组件都想知道朋友的在线状态。
现在我们已经把这部分逻辑提取到useFriendStatus的Hook中,那么我们可以这样使用:

  1. function FriendStatus(props) {
  2. const isOnline = useFriendStatus(props.friend.id);
  3. if (isOnline === null) {
  4. return 'Loading...';
  5. }
  6. return isOnline ? 'Online' : 'Offline';
  7. }
  1. function FriendListItem(props) {
  2. const isOnline = useFriendStatus(props.friend.id);
  3. return (
  4. <li style={{ color: isOnline ? 'green' : 'black' }}>
  5. {props.friend.name}
  6. </li>
  7. );
  8. }

这些代码和最初的例子等价吗?是的,他们的工作方式完全等同。如果你细看,你会注意到我们没有对这些行为作出任何改变。我们做的就是把两个函数的共有代码提取到另一个独立的函数中。自定义Hooks是遵循Hooks设计规则的自然衍生惯例,并不是一种React特性
必须要命名自己的Hook为useXXX吗?请这么做。这个惯例很重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了Hook规则。
两个组件使用了相同的Hook会共享状态吗?不,自定义Hooks是复用逻辑的机制(就像设置订阅或者记录当前值),但每一次使用自定义Hook的时候,它们内部所有的状态和作用都是隔离的。
自定义Hook是怎么隔离状态的?每一次我们调用Hook都会得到被隔离的状态。因为我们在直接调用useFriendStatus,从React的角度看,我们的组件还是在调用useState和useEffect而已。如同我们前些章节中所学,我们在同一个组件中调用useState和useEffect多次都是完全独立的。

提示:在Hooks间传递信息

既然Hooks是函数,我们就能在它们之间传递信息。
为了说明这一点,我们使用从我们假设的APP中的另一个组件。这是一个聊天对象选择器,她会显示当前选择的好友是否在线:

  1. const friendList = [
  2. { id: 1, name: 'Phoebe' },
  3. { id: 2, name: 'Rachel' },
  4. { id: 3, name: 'Ross' },
  5. ];
  6. function ChatRecipientPicker() {
  7. const [recipientID, setRecipientID] = useState(1);
  8. const isRecipientOnline = useFriendStatus(recipientID);
  9. return (
  10. <>
  11. <Circle color={isRecipientOnline ? 'green' : 'red'} />
  12. <select
  13. value={recipientID}
  14. onChange={e => setRecipientID(Number(e.target.value))}
  15. >
  16. {friendList.map(friend => (
  17. <option key={friend.id} value={friend.id}>
  18. {friend.name}
  19. </option>
  20. ))}
  21. </select>
  22. </>
  23. );
  24. }

我们保持当前选择的friend ID在状态变量recipientID中,并且在用户用select选择另一个friend的时候更新它。
因为useState Hook的使用可以给我们提供最新的recipientID的状态值,我们能把它传递给我们的自定义useFriendStatus中:

  1. const [recipientID, setRecipientID] = useState(1);
  2. const isRecipientOnline = useFriendStatus(recipientID);

如此可以让我们知道当前选中的好友是否在线。当我们选择不同的好友并更新 recipientID 状态变量时,useFriendStatusHook 将会取消订阅之前选中的好友,并订阅新选中的好友状态。

use你的想象

自定义Hooks提供了曾经React组件不曾拥有的共享逻辑的灵活性。你可以使用自定义Hooks来覆盖很多场景:处理、动画、订阅、定时器以及可能我们都不曾考虑的许许多多。更重要的是,我们能构建如同React原生特性一般简单的Hooks。
尽量去抵制那种过早就抽象的思维。函数组件现在能做的更多,那么你的代码库中函数组件的代码会变多。这正常——不要觉得你需要立即将组件拆分成Hooks。但是我们依旧鼓励你去发现那些可用Hooks隐藏复杂逻辑来提供简单接口的场景,或者解耦混乱的组件。
举例,也许你有一个复杂的组件,它包含了很多本地状态,这些状态已经被复杂的方式管理。useState不会集中管理你的更新逻辑,所以你可能小写一个类似Redux的reduce来:

  1. function todosReducer(state, action) {
  2. switch (action.type) {
  3. case 'add':
  4. return [...state, {
  5. text: action.text,
  6. completed: false
  7. }];
  8. // ... other actions ...
  9. default:
  10. return state;
  11. }
  12. }

Reducer非常方便独立测试,且对于传递复杂更新逻辑也更易于扩展。如果以后有必要,你可以把他们打散成更小的reducer。但是,你还想使用本地状态,或者不想安装新的第三方库。
所以为什么我们不写一个“useReducer”的Hook,来让我们能用reducer来管理本地状态?简单的版本可能是这样的:

  1. function useReducer(reducer, initialState) {
  2. const [state, setState] = useState(initialState);
  3. function dispatch(action) {
  4. const nextState = reducer(state, action);
  5. setState(nextState);
  6. }
  7. return [state, dispatch];
  8. }

现在,我们在组件中使用它,让 reducer 驱动它管理 state:

  1. function Todos() {
  2. const [todos, dispatch] = useReducer(todosReducer, []);
  3. function handleAddClick(text) {
  4. dispatch({ type: 'add', text });
  5. }
  6. // ...
  7. }

这种需要reducer子复杂组件中管理本地需求的场景很常见,所以我们将useReducer的Hook直接放到React中了。在下一讲API引用中,你将会发现它,以及其他内建的的Hooks。