一、共享逻辑场景及传统做法

构建自己的 Hook 能够将组件逻辑提取到可重用的函数中。

下面代码是 React Hook 文档中使用 Effect Hook 的时候构造的一个聊天应用程序,这个组件显示一条消息,指示朋友是在线还是离线:

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';
}

现在比如说聊天应用程序中也有一个联系人列表组件,而在这个列表中,想要把在线的用户名显示绿色。之前的做法可能是把上面订阅的逻辑在复制到 FriendListItem 组件中,不过这不是理想的做法:

import { useState, useEffect } from 'react';

function FriendListItem(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);
    };
  });

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

现在,我们希望在 FriendStatusFriendListItem 之间分享这个逻辑。

传统的做法中,React 有两种方式共享组件之间的 state 或者逻辑:render props高阶组件。但是使用 Hook 能够不在 tree 中添加更多组件的情况下解决相同的问题。

二、创建自定义 Hook

当我们想要在两个 JavaScript 函数之间共享逻辑,我们将其提取到第三个函数。组件和Hook 都是 function 这也适用于两者。

自定义 Hook 其实就是一个 JavaScript 方法,其名称以 “use” 开头,可以调用其他 Hook。例如下面的 useFriendStatus Hook 就是一个自定义的 Hook:

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;
}

上面没有任何新内容,逻辑是从上面的组件中复制的。就像是在组件中一样,确保只在自定义 Hook 的顶层无条件的调用其他 Hook。

和 React 组件不同的是,自定义的 Hook 不需要具有特定的 signature。我们可以决定它需要什么参数,以及返回什么内容(如果有返回值的话)换句话说,它就像是一个普通的 function。自定义 Hook 的名称应当始终以 use 开头,这样就能够直接去依照 Hook 的规则去使用。

我们使用 FriendStatus Hook 的目的是订阅我们朋友的在线状态,因此需要 friendID 作为参数,并返回好友是否在线。

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

  return isOnline;
}

三、使用自定义 Hook

最初,我们的目的是从 FriendStatusFriendListItem 组件中删除重复的逻辑,他们俩都想知道好友是否在线。

现在我们已经将这个逻辑提取到 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>
  );
}

1、上面代码是否功能相同?

是的,和之前的代码以相同的方式起作用。如果你仔细观察,可以发现对行为没有任何的变化。我们所做的只是将两个函数之间的一些公共代码提取到一个单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而不是 React 的功能。

2、是否必须以 use 开头设置自定义 Hook?

这是一个非常重要的约定,如果不以 use 开头,没有办法通过 lint 自动检查是否违反了 Hook 的规则,因为 React 无法判断某个函数是否包含对其中的 Hooks 调用。

3、使用相同的 Hook 的两个 Component 会共享 state 吗?

不会。

自定义 Hook 是一种重用有 state 逻辑的机制(比如设置订阅和记住当前值),但每次使用自定义 Hook 时,其中所有的 state 和 effect 都是完全隔离的。

4、自定义 Hook 如何获取隔离 state?

每次对 Hook 的调用都是被隔离的。因为我们直接调用 useFriendStatus,从 React 角度来说,我们的组件只调用了 useStateuseEffect

React 允许我们可以在一个组件中多次的调用 useStateuseEffect,它们是完全独立的。

四、Tip:在 Hook 之间传递信息

因为 Hook 是函数,我们可以在它们之间传递信息。

为了说明这一点,使用 mock 的聊天示例中的另一个组件。这是一个聊天消息收件人选择器,显示当前所选朋友是否在线:

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>
    </>
  );
}

我们将当前选择的 friendID 保留在 recipientID 状态变量中,如果用户在 <select> 选择器中选择其他好友,则需要更新它。

因为 useState Hook 调用为我们提供了 recipientID 状态变量的最新值,所以我们可以将它作为参数传递给我们的自定义 useFriendStatus Hook:

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

这让我们知道当前选择的好友是否在线。如果我们选择不同的朋友并更新 recipientID 状态变量,我们的 useFriendStatus Hook 将取消订阅之前选择的好友,并订阅新选择的好友的状态。

扩展

自定义 Hooks 提供了以前在 React 组件中无法实现的共享逻辑的灵活性。你可以编写自定义 Hook,涵盖广泛的用例,如:表单处理、动画、声明订阅、计时器等等。更重要的是,你可以构建于 React 的内置功能一样易于使用的 Hook。

尽量抵制过早添加抽象。既然 function 组件可以做的更多,那么代码库中的平均 function 组件可能变得很复杂。这是正常的 - 不要觉得必须将其分成 Hook。但是我们也鼓励你开始发现自定义 Hook 可以隐藏简单接口背后的复杂逻辑或帮助解开凌乱组件的情况。

比如,你可能有一个复杂的组件,其中包含许多以 ad-hoc 管理的本地 state。useState 不会使更新逻辑更容易集中化,因此你可能希望将其编写为 Redux reducer:

function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, {
        text: action.text,
        completed: false
      }];
    // ... other actions ...
    default:
      return state;
  }
}

reducer 非常便于单独测试,并且可以扩展以表达复杂的更新逻辑。如果有必要,你可以将它们分成更小的减速器。然后,你可能还享受使用 React 本地 state 的好处,不想使用其他库。

那么,如果我们可以编写一个 useReducer Hook,让我们使用 reducer 管理组件的本地 state。比如下面是一个可能的简化版:

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

然后我们可以在组件中使用它,让 reducer 驱动它的状态管理:

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer, []);

  function handleAddClick(text) {
    dispatch({ type: 'add', text });
  }

  // ...
}

在复杂组件中使用 reducer 管理本地状态的需求很常见,react 已经将 useReducer Hook构建到 React 中。更多可以参考 React Hook 的 API:https://reactjs.org/docs/hooks-reference.html