文档 / rescript-react / 构建自定义 Hooks
Edit

构建自定义 Hooks

React 自带了一些基础的 hooks,例如 React.useStateReact.useEffect。本章中你将学习如何针对你的 React 用例构建高阶 hooks。

为什么自定义 Hooks?

自定义 hooks 可以让你将现有组件的逻辑提取到可复用的,独立的函数中。

我们回顾一下之前的例子 React.useEffect 章节,这个例子中我们构建了一个聊天应用的组件,用于展示消息,显示朋友是否在线。

ReScriptJS Output
// FriendStatus.res

module ChatAPI = {
  // 出于演示的目的,ChatAPI是虚构的一个全局变量
  type status = { isOnline: bool };
  @val external subscribeToFriendStatus: (string, status => unit) => unit = "subscribeToFriendStatus";
  @val external unsubscribeFromFriendStatus: (string, status => unit) => unit = "unsubscribeFromFriendStatus";
}

type state = Offline | Loading | Online;

@react.component
let make = (~friendId: string) => {
  let (state, setState) = React.useState(_ => Offline)

  React.useEffect(() => {
    let handleStatusChange = (status) => {
      setState(_ => {
        status.ChatAPI.isOnline ? Online : Offline
      })
    }
 
    ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
    setState(_ => Loading);
 
    let cleanup = () => {
      ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
    }
 
    Some(cleanup)
  })

  let text = switch(state) {
    | Offline => friendId ++ " is offline"
    | Online => friendId ++ " is online"
    | Loading => "loading..."
  }

  <div>
    {React.string(text)}
  </div>
}

现在,假设我们的聊天应用有个联系人列表,我们想用绿色展示在线用户的名字。我们可以复制粘贴上面相似的逻辑到 FriendListItem 组件中,但这并不令人满意。

ReScriptJS Output
// FriendListItem.res
type state = Offline | Loading | Online;

// module ChatAPI = {...}

type friend = {
  id: string,
  name: string
};

@react.component
let make = (~friend: friend) => {
  let (state, setState) = React.useState(_ => Offline)

  React.useEffect(() => {
    let handleStatusChange = (status) => {
      setState(_ => {
        status.ChatAPI.isOnline ? Online : Offline
      })
    }
 
    ChatAPI.subscribeToFriendStatus(friend.id, handleStatusChange);
    setState(_ => Loading);
 
    let cleanup = () => {
      ChatAPI.unsubscribeFromFriendStatus(friend.id, handleStatusChange)
    }
 
    Some(cleanup)
  })

  let color = switch(state) {
    | Offline => "red"
    | Online => "green"
    | Loading => "grey"
  }

  <li style={ReactDOMStyle.make(~color,())}>
      {React.string(friend.name)}
  </li>
}

相反,我们更期望这段逻辑能在 FriendStatusFriendListItem 中共用

传统上,在 React 中有两种流行的方法来在组件间共享有状态的逻辑:render props 和高阶组件。现在让我们看看 Hooks 是如何解决大多数同类的问题,而不需要在组件树中添加更多组件的。

提取自定义 Hook

我们需要在函数之间共享逻辑时,通常会将逻辑提取到另一个函数中。组件和 Hooks 都是函数,所以这个思路也适用。

自定义 Hook 是以 ”use” 作为前缀的函数,同时也可能会调用其它的 Hooks。举例来说,下面的 useFriendStatus 是我们的第一个 Hook(我们新创建了 FriendStatusHook.res 文件,用来封装 state 类型):

ReScriptJS Output
// FriendStatusHook.res

// module ChatAPI {...}

type state = Offline | Loading | Online

let useFriendStatus = (friendId: string): state => {
  let (state, setState) = React.useState(_ => Offline)

  React.useEffect(() => {
    let handleStatusChange = status => {
      setState(_ => {
        status.ChatAPI.isOnline ? Online : Offline
      })
    }

    ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange)
    setState(_ => Loading)

    let cleanup = () => {
      ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
    }

    Some(cleanup)
  })

  state
}

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

与 React 组件不同,自定义 Hook 不需要具有特定签名。 我们可以决定它接受什么作为参数,以及它应该返回什么(如果有的话)。换句话说,它就像一个普通的函数。它的名称应该始终以 use 开头,以便你一眼就可以看出它是 Hooks。

我们的 useFriendStatus Hook 的用途是订阅朋友的状态。所以它将 friendId 作为参数,并返回在线状态,如 OnlineOfflineLoading

RES
let useFriendStatus = (friendId: string): status { let (state, setState) = React.useState(_ => Offline); // ... state }

现在用一下我们的自定义 Hook。

使用自定义 Hook

最开始我们的既定目标是移除 FriendStatusFriendListItem 组件的重复逻辑。这两个组件都需要知道朋友的在线状态。

现在我们已经将这个逻辑提取到 useFriendStatus hook 中,我们可以直接使用它:

ReScriptJS Output
// FriendStatus.res
type friend = { id: string };

@react.component
let make = (~friend: friend) => {
  let onlineState = FriendStatusHook.useFriendStatus(friend.id);

  let status = switch(onlineState) {
    | FriendStatusHook.Online => "Online"
    | Loading => "Loading"
    | Offline => "Offline"
  }

  React.string(status);
}
ReScriptJS Output
// FriendListItem.res
@react.component
let make = (~friend: friend) => {
  let onlineState = FriendStatusHook.useFriendStatus(friend.id);

  let color = switch(onlineState) {
    | Offline => "red"
    | Online => "green"
    | Loading => "grey"
  }

  <li style={ReactDOMStyle.make(~color,())}>
      {React.string(friend.name)}
  </li>
}

这个代码和之前的例子是等价的吗?是的,它会以相同的方式工作。如果你仔细观察,你会注意到我们没有更改任何行为。我们只是将两个函数之间的一些公共代码提取到一个单独的函数中。自定义 Hooks 是一个约定,自然遵循 Hooks 的设计,而不是一个 React 特性。

我的自定义 Hooks 必须以 “use” 开头吗?请这样做吧。这个约定非常重要。没有它,我们就无法自动检查是否违反 Hooks 规则,因为我们无法判断某个函数内部是否包含对 Hooks 的调用。

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

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

技巧:在 Hooks 之间传递信息

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

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

ReScriptJS Output
type friend = {id: string, name: string}

let friendList = [
  {id: "1", name: "Phoebe"},
  {id: "2", name: "Rachel"},
  {id: "3", name: "Ross"},
]

@react.component
let make = () => {
  let (recipientId, setRecipientId) = React.useState(_ => "1")
  let recipientOnlineState = FriendStatusHook.useFriendStatus(recipientId)

  let color = switch recipientOnlineState {
  | FriendStatusHook.Offline => Circle.Red
  | Online => Green
  | Loading => Grey
  }

  let onChange = evt => {
    let value = ReactEvent.Form.target(evt)["value"]
    setRecipientId(value)
  }

  let friends = Belt.Array.map(friendList, friend => {
    <option key={friend.id} value={friend.id}>
      {React.string(friend.name)}
    </option>
  })

  <>
    <Circle color />
    <select value={recipientId} onChange>
      {React.array(friends)}
    </select>
  </>
}

我们将当前选择的朋友 ID 保存在 recipientId 状态变量中,如果用户在 <select> 选择器中选择了不同的朋友,则更新它。

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

RES
let (recipientId, setRecipientId) = React.useState(_ => "1") let recipientOnlineState = FriendStatusHook.useFriendStatus(recipientId)

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

发挥你的想象力

自定义 Hooks 提供了共享逻辑的灵活性。你可以编写自定义 Hook 去覆盖广泛的用例,例如表单处理、动画、声明式订阅、计时器,以及更多我们没有考虑到的用例。更重要的是,你可以构建与 React 的内置功能一样易于使用的 Hooks。

尽量避免过早地添加抽象。当涉及到足够多的状态逻辑处理时,组件通常会变得很臃肿。这很正常,不要觉得必须立即将其拆分为 Hooks。但我们也鼓励你去发现自定义 Hook 的应用场景,它可以将复杂逻辑隐藏在简单的接口后面或帮助你解耦杂乱的组件。