构建你自己的 Hooks

Hooks 是一项新功能提案,可让您在不编写类的情况下使用 state(状态) 和其他 React 功能。它们目前处于 React v16.7.0-alpha 中,并在 一个开放RFC 中进行讨论。

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

当我们学习 使用 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 中,我们有两种流行的方式来共享组件之间的状态逻辑:render props(渲染属性)higher-order components(高阶组件)。 我们现在将看看 Hook 如何在不强制您向树中添加更多组件的情况下解决许多相同的问题。

提取自定义 Hook

当我们想要在两个 JavaScript 函数之间共享逻辑时,我们会将共享逻辑提取到第三个函数。 组件和 Hook 都是函数,所以这种办法也适用于它们!

自定义 Hook 是一个 JavaScript 函数,其名称以 ”use” 开头,可以调用其他 Hook。 例如,下面的 useFriendStatus 是我们的第一个自定义 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 不需要特定的签名。我们可以决定它接受什么作为参数,以及它应该返回什么(如果有的话)。换句话说,它就像一个普通的函数。它的名称应该总是以 use 开头,这样您就可以一眼看出Hooks 的规则 适用于它。

我们使用 useFriendStatus Hook 的目的是订阅一个朋友的状态。这就是为什么它将 friendID 作为参数,并返回该朋友是否在线:

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

  return isOnline;
}

现在让我们看看如何使用我们的自定义 Hook 。

使用自定义 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>
  );
}

这段代码等价于原始示例吗? 是的,它以完全相同的方式工作。如果你仔细观察,你会发现我们没有对行为做任何改变。我们所做的只是将两个函数之间的一些公共代码提取到一个单独的函数中。自定义 Hooks 是一种惯例,它自然地遵循 Hooks 设计的约定,而不是 React 特性

我必须以 “use” 开头命名我的自定义 Hooks 吗? 请使用 use 开头。这个习惯非常重要。如果没有它,我们就不能自动检查该 Hook 是否违反了 Hooks 的规则,因为我们无法判断某个函数是否包含对其内部 Hooks 的调用。

两个组件使用相同的 Hook 共享 state(状态) 吗? 不会。自定义 Hooks 是一种重用 stateful(有状态) 逻辑 的机制(例如设置订阅和记住当前值),但是每次使用自定义 Hook 时,它内部的所有状态和效果都是完全隔离的。

自定义 Hook 如何获得隔离 state(状态) ? 每次对 Hook 的调用都会被隔离。因为我们直接调用 useFriendStatus ,从 React 的角度来看,我们的组件只调用 useStateuseEffect 。正如我们 之前 所学到的 的,我们可以在一个组件中多次调用 useStateuseEffect ,它们将完全独立的。

小技巧: 提示:在 Hooks 之间传递信息

由于 Hooks 是函数,所以我们可以在它们之间传递信息。

为了说明这一点,我们将使用另一个组件,该组件来自假想聊天示例。这是一个聊天消息收件人选择器,显示当前选择的朋友是否在线:

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 state(状态) 变量中,如果用户在 <select> 选择器中选择了不同的好友,则更新它。

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

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

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

useYourImagination()

自定义 Hooks 提供了以前在 React 组件中不可能实现的共享逻辑的灵活性。您可以编写自定义 Hooks ,这些 Hooks 涵盖了广泛的用例,如表单处理、动画、声明性订阅、计时器,可能还有许多我们没有考虑过的用例。此外,您还可以构建与 React 内置特性一样易用的 Hooks 。

不要过早地添加抽象。既然函数式组件可以做更多的事情,那么代码库中的普通的函数式组件可能会变得更长。这是正常的,不要觉得你 必须 立即把将它分成 Hooks。但是我们也鼓励您开始发现一些好的事情,比如,自定义 Hooks 可以在简单的接口背后隐藏复杂的逻辑,或者帮助理清凌乱的组件。

例如,您可能有一个复杂的组件,它包含许多以特殊方式管理的本地状态。useState 并没有使集中更新逻辑变得更容易,所以您可能更愿意将其编写为 Redux reducer:

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

Reducers 非常便于单独测试,并且可以扩展以表达复杂的更新逻辑。如有必要,可以将它们分成更小的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 驱动它的 state(状态) 管理:

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

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

  // ...
}

在复杂组件中使用 reducer 管理本地 state(状态) 的需求很常见,我们已经将 useReducer 钩子直接构建到 React 中。您将在 Hooks API 参考 中找到它和其他内置 Hooks 。