十、构建自己的挂钩
在上一章中,我们了解了挂钩的限制和规则。我们学习了在哪里调用挂钩,为什么挂钩的顺序很重要,以及挂钩的命名约定。最后,我们学习了如何强制执行挂钩规则和处理useEffect
依赖关系。
在本章中,我们将学习如何通过从组件中提取现有代码来创建自定义挂钩。我们还将学习如何使用自定义挂钩以及挂钩如何相互交互。然后,我们将学习如何为自定义挂钩编写测试。最后,我们将学习完整的 React Hooks API。
本章将介绍以下主题:
- 提取自定义挂钩
- 使用自定义挂钩
- 挂钩之间的相互作用
- 测试挂钩
- 探索 React 挂钩 API
技术要求
应该已经安装了 Node.js 的最新版本(v11.12.0 或更高版本)。Node.js 的npm
包管理器也需要安装。
本章代码可在 GitHub 上找到:https://github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter10 。
请查看以下视频以查看代码的运行情况:
Please note that it is highly recommended that you write the code on your own. Do not simply run the code examples provided previously. It is important to write the code yourself in order to learn and understand properly. However, if you run into any issues, you can always refer to the code example.
现在让我们从这一章开始。
提取自定义挂钩
在通过学习状态和效果挂钩、社区挂钩和挂钩规则,很好地掌握了挂钩的概念之后,我们现在将构建自己的挂钩。我们首先从博客应用的现有功能中提取定制挂钩。通常,如果我们注意到我们在多个组件中使用了类似的代码,那么最好先编写组件,然后再从中提取自定义挂钩。这样做可以避免过早地定义自定义挂钩,并使项目变得不必要的复杂。
我们将在本节中提取以下挂钩:
- 钩
useUserState
和usePostsState
挂钩- 钩
- API 挂钩
- 钩
创建 useTheme 挂钩
在许多组件中,我们使用ThemeContext
来设计我们的博客应用。跨多个组件使用的功能通常是创建自定义挂钩的好机会。您可能已经注意到,我们通常会执行以下操作:
import { ThemeContext } from '../contexts'
export default function SomeComponent () {
const theme = useContext(ThemeContext)
// ...
我们可以将此功能抽象为一个useTheme
挂钩,它将从ThemeContext
获取theme
对象。
让我们开始创建一个定制的useTheme
挂钩:
- 创建一个新的
src/hooks/
目录,这是我们将要放置自定义挂钩的地方。 - 创建一个新的
src/hooks/useTheme.js
文件。 - 在这个新创建的文件中,我们首先导入
useContext
挂钩和ThemeContext
,如下所示:
import { useContext } from 'react'
import { ThemeContext } from '../contexts'
- 接下来,我们导出一个名为
useTheme
的新函数;这将是我们的定制挂钩。记住,挂钩只是以use
关键字为前缀的函数:
export default function useTheme () {
- 在我们的定制挂钩中,我们现在可以使用 React 提供的基本挂钩来构建我们自己的挂钩。在本例中,我们只需返回
useContext
挂钩:
return useContext(ThemeContext)
}
正如我们所看到的,定制挂钩可以非常简单。在这种情况下,定制挂钩只返回一个传递了ThemeContext
的上下文挂钩。尽管如此,这使得我们的代码更加简洁,以后更容易修改。此外,通过使用useTheme
挂钩,很明显我们想要访问主题,这意味着我们的代码将更易于阅读和推理。
创建全局状态挂钩
我们经常做的另一件事是进入全球状态。例如,有些组件需要user
状态,有些组件需要posts
状态。为了抽象此功能(这也将使以后更容易调整状态结构),我们可以创建自定义挂钩来获取状态的某些部分:
useUserState
:获取state
对象的user
部分usePostsState
:获取state
对象的posts
部分
定义 useUserState 挂钩
重复我们对useTheme
挂钩所做的类似过程,我们从 React 和StateContext
导入useContext
挂钩。但是,我们现在不是返回上下文挂钩的结果,而是通过解构拉出state
对象,然后返回state.user
。
创建具有以下内容的新src/hooks/useUserState.js
文件:
import { useContext } from 'react'
import { StateContext } from '../contexts'
export default function useUserState () {
const { state } = useContext(StateContext)
return state.user
}
与useTheme
挂钩类似,useUserState
挂钩使我们的代码更加简洁,以后更容易修改,并提高可读性。
定义 usePostsState 挂钩
我们对posts
状态重复相同的过程。创建一个新的src/hooks/usePostsState.js
文件,包含以下内容:
import { useContext } from 'react'
import { StateContext } from '../contexts'
export default function usePostsState () {
const { state } = useContext(StateContext)
return state.posts
}
与useTheme
和useUserState
挂钩类似,usePostsState
挂钩使我们的代码更加简洁,以后更容易修改,并提高可读性。
创建 useDispatch 挂钩
在许多组件中,我们需要dispatch
函数来执行某些操作,因此我们通常需要执行以下操作:
import { StateContext } from '../contexts'
export default function SomeComponent () {
const { dispatch } = useContext(StateContext)
// ...
我们可以将此功能抽象为一个useDispatch
挂钩,它将从全局状态上下文中获取dispatch
函数。这样做还将使以后更容易替换状态管理实现。例如,稍后,我们可以用状态管理库(如 Redux 或 MobX)替换简单的 Reducer 挂钩。
现在让我们使用以下步骤定义useDispatch
挂钩:
- 创建一个新的
src/hooks/useDispatch.js
文件。 - 从 React 和
StateContext
导入useContext
挂钩,如下所示:
import { useContext } from 'react'
import { StateContext } from '../contexts'
- 接下来,我们定义并导出
useDispatch
函数;在这里,我们允许传递一个不同的context
作为参数,以使挂钩更通用(如果稍后我们想从本地状态上下文中使用dispatch
函数)。但是,我们将context
参数的默认值设置为StateContext
,如下所示:
export default function useDispatch (context = StateContext) {
- 最后,我们通过解构从上下文挂钩中拉出
dispatch
函数,并返回以下代码:
const { dispatch } = useContext(context)
return dispatch
}
正如我们所看到的,创建一个定制的分派挂钩使我们的代码在以后更容易更改,因为我们只需要在一个地方调整dispatch
函数。
创建 API 挂钩
我们还可以为各种 API 调用创建挂钩。将这些挂钩放在一个文件中可以让我们以后轻松地调整 API 调用。我们将用useAPI
作为自定义 API 挂钩的前缀,这样很容易区分哪些函数是 API 挂钩。
现在,让我们使用以下步骤为 API 创建自定义挂钩:
- 创建一个新的
src/hooks/api.js
文件。 - 从
react-request-hook
库导入useResource
挂钩,如下所示:
import { useResource } from 'react-request-hook'
- 首先,我们定义一个
useAPILogin
挂钩来登录用户;我们只需从src/user/Login.js
文件中剪切并粘贴现有代码,如下所示:
export function useAPILogin () {
return useResource((username, password) => ({
url: `/login/${encodeURI(username)}/${encodeURI(password)}`,
method: 'get'
}))
}
- 接下来,我们定义一个
useAPIRegister
挂钩;我们只需从src/user/Register.js
文件中剪切并粘贴现有代码,如下所示:
export function useAPIRegister () {
return useResource((username, password) => ({
url: '/users',
method: 'post',
data: { username, password }
}))
}
- 现在我们定义一个
useAPICreatePost
挂钩,从src/post/CreatePost.js
文件中剪切和粘贴现有代码,如下所示:
export function useAPICreatePost () {
return useResource(({ title, content, author }) => ({
url: '/posts',
method: 'post',
data: { title, content, author }
}))
}
- 最后,我们定义了一个
useAPIThemes
挂钩,将src/ChangeTheme.js
文件中已有的代码剪切粘贴如下:
export function useAPIThemes () {
return useResource(() => ({
url: '/themes',
method: 'get'
}))
}
正如我们所看到的,将所有 API 相关功能放在一个地方可以使以后调整 API 代码更加容易。
创建 UsedBounceUndo 挂钩
我们现在将创建一个稍微高级一点的挂钩来实现取消公告的撤销功能。我们已经在CreatePost
组件中实现了这个功能。现在,我们将把这个功能提取到一个定制的useDebouncedUndo
挂钩中。
让我们通过以下步骤创建useDebouncedUndo
挂钩:
- 创建一个新的
src/hooks/useDebouncedUndo.js
文件。 - 从 React 导入
useState
、useEffect
和useCallback
挂钩,以及useUndo
挂钩和useDebouncedCallback
挂钩:
import { useState, useEffect, useCallback } from 'react'
import useUndo from 'use-undo'
import { useDebouncedCallback } from 'use-debounce'
- 现在我们将定义
useDebouncedUndo
函数,该函数接受一个timeout
参数用于取消公告回调:
export default function useDebouncedUndo (timeout = 200) {
- 在这个函数中,我们复制了前面实现中的
useState
挂钩,如下所示:
const [ content, setInput ] = useState('')
- 接下来,我们在
useUndo
挂钩上复制;但是,这一次,我们将所有其他与撤销相关的函数存储在一个undoRest
对象中:
const [ undoContent, { set: setContent, ...undoRest } ] = useUndo('')
- 然后我们复制
useDebouncedCallback
挂钩,用timeout
参数替换固定的200
值:
const [ setDebounce, cancelDebounce ] = useDebouncedCallback(
(value) => {
setContent(value)
},
timeout
)
- 现在我们复制效果挂钩,如下代码所示:
useEffect(() => {
cancelDebounce()
setInput(undoContent.present)
}, [cancelDebounce, undoContent])
- 然后,我们定义一个
setter
函数,它将设置一个新的输入value
并调用setDebounce
。我们可以在这里用一个useCallback
挂钩包装setter
函数,以返回该函数的记忆版本,并避免每次使用挂钩的组件重新渲染时都重新创建该函数。与useEffect
和useMemo
挂钩类似,我们也传递一个依赖数组作为useCallback
挂钩的第二个参数:
const setter = useCallback(function setterFn (value) {
setInput(value)
setDebounce(value)
}, [ setInput, setDebounce ])
- 最后,我们返回
content
变量(包含当前输入value
)、setter
函数和undoRest
对象(包含undo
/redo
函数和canUndo
/canRedo
布尔值):
return [ content, setter, undoRest ]
}
创建一个自定义挂钩用于取消绑定撤消意味着我们可以跨多个组件重用该功能。我们甚至可以将这个挂钩作为公共库提供,允许其他人轻松实现取消公告的撤销/重做功能。
出口我们的定制挂钩
在创建了所有自定义挂钩之后,我们将在挂钩目录中创建一个index.js
文件,并在那里重新导出挂钩,这样我们就可以按如下方式导入自定义挂钩:import { useTheme } from './hooks'
现在,让我们使用以下步骤导出所有自定义挂钩:
- 创建一个新的
src/hooks/index.js
文件。 - 在此文件中,我们首先导入自定义挂钩,如下所示:
import useTheme from './useTheme'
import useDispatch from './useDispatch'
import usePostsState from './usePostsState'
import useUserState from './useUserState'
import useDebouncedUndo from './useDebouncedUndo'
- 然后,我们使用以下代码重新导出这些导入的挂钩:
export { useTheme, useDispatch, usePostsState, useUserState, useDebouncedUndo }
- 最后,我们重新导出
api.js
文件中的所有挂钩,如下所示:
export * from './api'
现在我们已经导出了所有的定制挂钩,我们可以直接从hooks
文件夹导入挂钩,这样一次导入多个定制挂钩就更容易了。
示例代码
示例代码可在Chapter10/chapter10_1
文件夹中找到。
只需运行npm install
安装所有依赖项,运行npm start
启动应用,然后在浏览器中访问http://localhost:3000
(如果它没有自动打开)。
使用我们的定制挂钩
在创建自定义挂钩之后,我们现在可以开始在整个博客应用中使用它们。使用自定义挂钩非常简单,因为它们类似于社区挂钩。与所有其他挂钩一样,自定义挂钩只是 JavaScript 函数。
我们创建了以下挂钩:
useTheme
useDispatch
usePostsState
useUserState
useDebouncedUndo
useAPILogin
useAPIRegister
useAPICreatePost
useAPIThemes
在本节中,我们将对应用进行重构,以使用所有自定义挂钩。
使用 useTheme 挂钩
我们现在可以直接使用useTheme
挂钩,而不是将useContext
挂钩与ThemeContext
挂钩一起使用!如果我们以后改变主题系统,我们可以简单地修改useTheme
挂钩,我们的新系统将在整个应用中实现。
让我们重构我们的应用以使用useTheme
挂钩:
- 编辑
src/Header.js
并用useTheme
挂钩的导入替换现有导入。可以删除ThemeContext
和useContext
导入项:
import { useTheme } from './hooks'
- 然后,将当前上下文挂钩定义替换为
useTheme
挂钩,如下所示:
const { primaryColor } = useTheme()
- 现在编辑
src/post/Post.js
并在此处类似地调整导入:
import { useTheme } from './hooks'
- 然后,将
useContext
吊钩更换为useTheme
吊钩,如下所示:
const { secondaryColor } = useTheme()
正如我们所看到的,使用自定义挂钩使我们的代码更加简洁和易于阅读。现在我们继续使用全局状态挂钩。
使用全局状态挂钩
类似于我们对ThemeContext
所做的,我们也可以用usePostsState
、useUserState
和useDispatch
挂钩替换我们的状态上下文挂钩。如果我们想稍后更改状态逻辑,这是最佳的。例如,如果我们的州在增长,我们想使用更复杂的系统,比如 Redux 或 MobX,那么我们可以简单地调整现有的挂钩,一切都会像以前一样工作。
在本节中,我们将调整以下组件:
UserBar
Login
Register
Logout
CreatePost
PostList
调整用户栏组件
首先,我们要调整UserBar
组件。在这里,我们可以通过以下步骤使用useUserState
挂钩:
- 编辑
src/user/UserBar.js
并导入useUserState
挂钩:
import { useUserState } from '../hooks'
- 然后,我们删除以下挂钩定义:
const { state } = useContext(StateContext)
const { user } = state
- 我们将其替换为我们的定制
useUserState
挂钩:
const user = useUserState()
现在,UserBar
组件使用我们的定制挂钩,而不是直接访问user
状态。
调整登录组件
接下来,我们要调整Login
组件,在这里我们可以使用useDispatch
挂钩。以下步骤概述了该过程:
- 编辑
src/user/Login.js
并导入useDispatch
挂钩,如下所示:
import { useDispatch } from '../hooks'
- 然后移除以下上下文挂钩:
const { dispatch } = useContext(StateContext)
- 替换为我们定制的
useDispatch
挂钩:
const dispatch = useDispatch()
现在,Login
组件使用我们的定制挂钩,而不是直接访问dispatch
函数。接下来,我们将调整Register
组件。
调整寄存器组件
与Login
组件类似,我们也可以在Register
组件中使用useDispatch
挂钩,如下步骤所示:
- 编辑
src/user/Register.js
并导入useDispatch
挂钩:
import { useDispatch } from '../hooks'
- 然后,用我们的自定义分派挂钩替换当前上下文挂钩,如下所示:
const dispatch = useDispatch()
现在Register
组件也使用了我们的定制挂钩,而不是直接访问dispatch
函数。
调整注销组件
然后,我们将通过以下步骤调整Logout
组件,以同时使用useUserState
和useDispatch
挂钩:
- 编辑
src/user/Logout.js
并导入useUserState
和useDispatch
挂钩:
import { useDispatch, useUserState } from '../hooks'
- 然后,将当前挂钩定义替换为以下内容:
const dispatch = useDispatch()
const user = useUserState()
现在,Logout
组件使用我们的定制挂钩,而不是直接访问user
状态和dispatch
函数。
调整 CreatePost 组件
接下来我们将调整CreatePost
组件,这与我们对Logout
组件所做的类似。以下步骤概述了该过程:
- 编辑
src/post/CreatePost.js
并导入useUserState
和useDispatch
挂钩:
import { useUserState, useDispatch } from '../hooks'
- 然后,将当前上下文挂钩定义替换为以下内容:
const user = useUserState()
const dispatch = useDispatch()
现在,CreatePost
组件使用我们的定制挂钩,而不是直接访问user
状态和dispatch
函数。
调整 PostList 组件
最后,我们将使用usePostsState
挂钩渲染PostList
组件,如下所示:
- 编辑
src/post/PostList.js
并导入usePostsState
挂钩:
import { usePostsState } from '../hooks'
- 然后将当前挂钩定义替换为以下内容:
const posts = usePostsState()
现在,PostList
组件使用我们的定制挂钩,而不是直接访问posts
状态。
使用 API 挂钩
接下来,我们将用我们的定制 API 挂钩替换所有的useResource
挂钩。这样做使我们可以将所有 API 调用都放在一个文件中,以便以后在 API 发生变化时可以轻松地调整它们。
在本节中,我们将调整以下组件:
ChangeTheme
Register
Login
CreatePost
让我们开始吧。
调整 ChangeTheme 组件
首先,我们将调整ChangeTheme
组件并替换资源挂钩,在以下步骤中使用我们的自定义useAPIThemes
挂钩访问/themes
:
- 在
src/ChangeTheme.js
中,删除以下useResource
挂钩导入语句:
import { useResource } from 'react-request-hook'
替换为我们定制的useAPIThemes
挂钩:
import { useAPIThemes } from './hooks'
- 然后,将
useResource
挂钩定义替换为以下自定义挂钩:
const [ themes, getThemes ] = useAPIThemes()
现在,ChangeTheme
组件使用我们定制的 API 挂钩从 API 中提取主题。
调整寄存器组件
接下来,我们将按照以下步骤调整Register
组件:
- 编辑
src/user/Register.js
并调整 import 语句以同时导入useAPIRegister
挂钩:
import { useDispatch, useAPIRegister } from '../hooks'
- 然后,将当前资源挂钩替换为以下内容:
const [ user, register ] = useAPIRegister()
现在,Register
组件通过 API 向register
用户使用我们的定制 API 挂钩。
调整登录组件
与Register
组件类似,我们也将调整Login
组件:
- 编辑
src/user/Login.js
并调整 import 语句以同时导入useAPILogin
挂钩:
import { useDispatch, useAPILogin } from '../hooks'
- 然后,将当前资源挂钩替换为以下内容:
const [ user, login ] = useAPILogin()
现在,Login
组件使用我们定制的 API 挂钩通过 API 登录用户。
调整 CreatePost 组件
最后,我们将按照以下步骤调整CreatePost
组件:
- 编辑
src/post/CreatePost.js
并调整 import 语句以同时导入useAPICreatePost
挂钩:
import { useUserState, useDispatch, useAPICreatePost } from '../hooks'
- 然后,将当前资源挂钩替换为以下内容:
const [ post, createPost ] = useAPICreatePost()
现在,CreatePost
组件使用我们的定制 API 挂钩通过 API 创建新帖子。
使用 UsedBounceUndo 挂钩
最后,我们将用自定义的useDebouncedUndo
挂钩替换src/post/CreatePost.js
文件中所有取消绑定的撤销逻辑。这样做将使我们的组件代码更干净、更易于阅读。此外,我们以后可以在其他组件中重用相同的取消公告撤消功能。
让我们按照以下步骤开始使用CreatePost
组件中的去抖动撤消挂钩:
- 编辑
src/post/CreatePost.js
并导入useDebouncedUndo
挂钩:
import { useUserState, useDispatch, useDebouncedUndo, useAPICreatePost } from '../hooks'
- 然后,删除与取消公告撤消处理相关的以下代码:
const [ content, setInput ] = useState('')
const [ undoContent, {
set: setContent,
undo,
redo,
canUndo,
canRedo
} ] = useUndo('')
const [ setDebounce, cancelDebounce ] = useDebouncedCallback(
(value) => {
setContent(value)
},
200
)
useEffect(() => {
cancelDebounce()
setInput(undoContent.present)
}, [cancelDebounce, undoContent])
更换为我们定制的useDebouncedUndo
挂钩,如下所示:
const [ content, setContent, { undo, redo, canUndo, canRedo } ] = useDebouncedUndo()
- 最后,删除
handleContent
函数中的以下 setter 函数(用粗体标记):
function handleContent (e) {
const { value } = e.target
setInput(value)
setDebounce(value)
}
我们现在可以使用我们的定制挂钩提供的setContent
功能:
function handleContent (e) {
const { value } = e.target
setContent(value)
}
正如您所看到的,我们的代码现在更干净、更简洁、更易于阅读。此外,我们可以在以后的其他组件中重用取消绑定的撤销挂钩。
示例代码
示例代码可在Chapter10/chapter10_2
文件夹中找到。
只需运行npm install
安装所有依赖项,运行npm start
启动应用,然后在浏览器中访问http://localhost:3000
(如果它没有自动打开)。
挂钩之间的相互作用
我们的整个博客应用现在的工作方式与以前相同,但它使用我们的自定义挂钩!到目前为止,我们一直有封装整个逻辑的挂钩,只有常量值作为参数传递给我们的自定义挂钩。但是,我们也可以将其他挂钩的值传递到自定义挂钩中!
Since Hooks are simply JavaScript functions, all Hooks can accept any value as arguments and work with them: constant values, component props, or even values from other Hooks.
我们现在将创建本地挂钩,这意味着它们将与组件放在同一个文件中,因为其他地方不需要它们。然而,它们仍然使我们的代码更易于阅读和维护。这些本地挂钩将接受来自其他挂钩的值作为参数。
将创建以下本地挂钩:
- 本地寄存器效应挂钩
- 本地登录效果挂钩
让我们在下面的小节中了解如何创建它们。
创建本地寄存器效果挂钩
首先,我们将从Login
组件中提取效果挂钩到一个单独的useRegisterEffect
挂钩函数。此函数将接受来自其他挂钩的以下值作为参数:user
和dispatch
。
现在让我们使用以下步骤为Register
组件创建一个局部效果挂钩:
- 在导入语句之后,编辑
src/user/Register.js
并在组件函数之外定义一个新函数:
function useRegisterEffect (user, dispatch) {
- 对于函数的内容,从
Register
组件中剪切现有的效果挂钩并粘贴在此处:
useEffect(() => {
if (user && user.data) {
dispatch({ type: 'REGISTER', username: user.data.username })
}
}, [dispatch, user])
}
- 最后,定义我们的自定义
useLoginEffect
挂钩,在这里我们剪切掉前面的效果挂钩,并将其他挂钩的值传递给它:
useRegisterEffect(user, dispatch)
正如我们所见,将效果提取到单独的函数中可以使代码更易于阅读和维护。
创建本地登录效果挂钩
重复与本地寄存器效果挂钩类似的过程,我们还将把效果挂钩从Login
组件提取到一个单独的useLoginEffect
挂钩函数。此函数将接受来自其他挂钩的以下值作为参数:user
、dispatch
和setLoginFailed
。
现在让我们使用以下步骤为Login
组件创建一个本地挂钩:
- 在导入语句之后,编辑
src/user/Login.js
并在组件函数之外定义一个新函数:
function useLoginEffect (user, dispatch, setLoginFailed) {
- 对于函数的内容,从
Login
组件中剪切现有的效果挂钩并粘贴在此处:
useEffect(() => {
if (user && user.data) {
if (user.data.length > 0) {
setLoginFailed(false)
dispatch({ type: 'LOGIN', username: user.data[0].username })
} else {
setLoginFailed(true)
}
}
if (user && user.error) {
setLoginFailed(true)
}
}, [dispatch, user, setLoginFailed])
}
Here, we also added setLoginFailed
to the Effect Hook dependencies. This is to make sure that whenever the setter
function changes (which could happen eventually when using the Hook) the Hook triggers again. Always passing all dependencies of an Effect Hook, including functions, prevents bugs and unexpected behavior later on.
- 最后,定义我们的自定义
useLoginEffect
挂钩,在这里我们剪切掉前面的效果挂钩,并将其他挂钩的值传递给它:
useLoginEffect(user, dispatch, setLoginFailed)
正如我们所见,将效果提取到单独的函数中可以使代码更易于阅读和维护。
示例代码
示例代码可在Chapter10/chapter10_3
文件夹中找到。
只需运行npm install
安装所有依赖项,运行npm start
启动应用,然后在浏览器中访问http://localhost:3000
(如果它没有自动打开)。
测试挂钩
现在我们的博客应用充分利用了挂钩!我们甚至为各种函数定义了自定义挂钩,以使我们的代码更加可重用、简洁和易于阅读。
在定义自定义挂钩时,为它们编写测试以确保它们正常工作是有意义的,即使我们以后更改它们或添加更多选项时也是如此。
为了测试我们的挂钩,我们将使用 Jest 测试运行程序,它包含在我们的create-react-app
项目中。然而,由于挂钩的规则,我们不能从测试函数调用挂钩,因为它们只能在函数组件的主体内部调用。
因为我们不想专门为每个测试创建一个组件,所以我们将使用 React Hooks 测试库直接测试挂钩。这个库实际上创建了一个测试组件,并提供了各种与挂钩交互的实用程序函数。
使用 React 挂钩测试库
除了 React 挂钩测试库之外,我们还需要一个特殊的 React 渲染器。为了向 DOM 呈现 React 组件,我们使用了react-dom
;对于测试,我们可以使用react-test-renderer
。我们现在将通过npm
安装 React Hooks 测试库和react-test-renderer
:
> npm install --save-dev @testing-library/react-hooks react-test-renderer
应在以下情况下使用 React Hooks 测试库:
- 在编写定义挂钩的库时
- 在多个组件中使用挂钩时(全局挂钩)
但是,当挂钩仅在单个组件(本地挂钩)中定义和使用时,不应使用该库。
在这种情况下,我们应该使用 React 测试库直接测试组件。但是,测试 React 组件超出了本书的范围。有关测试组件的更多信息,请访问图书馆网站:https://testing-library.com/docs/react-testing-library/intro 。
测试简单挂钩
首先,我们将测试一个非常简单的挂钩,它不使用上下文或异步代码,比如超时。为此,我们将创建一个名为useCounter
的新挂钩。然后,我们将测试挂钩的各个部分。
本节将介绍以下任务:
- 创建
useCounter
挂钩 - 测试结果
- 测试吊钩动作
- 测试初始值
- 测试重置和强制重新渲染
我们现在就开始吧。
创建 useCounter 挂钩
useCounter
挂钩将向increment
和reset
计数器提供电流count
和功能。
现在让我们使用以下步骤创建useCounter
挂钩:
- 创建一个新的
src/hooks/useCounter.js
文件。 - 从 React 导入
useState
和useCallback
挂钩,如下所示:
import { useState, useCallback } from 'react'
- 我们定义了一个新的
useCounter
挂钩函数,其中包含一个initialCount
参数:
export default function useCounter (initialCount = 0) {
- 然后,我们用以下代码为
count
值定义一个新的状态挂钩:
const [ count, setCount ] = useState(initialCount)
- 接下来,我们定义
count
的递增和重置函数,如下所示:
const increment = useCallback(() => setCount(count + 1), [])
const reset = useCallback(() => setCount(initialCount), [initialCount])
- 最后返回当前
count
和两个函数:
return { count, increment, reset }
}
现在我们已经定义了一个简单的挂钩,我们可以开始测试它了。
测试计数器挂钩结果
现在让我们按照以下步骤为我们创建的useCounter
挂钩编写测试:
- 创建一个新的
src/hooks/useCounter.test.js
文件。 - 从 React Hooks 测试库导入
renderHook
和act
函数,因为我们稍后将使用这些函数:
import { renderHook, act } from '@testing-library/react-hooks'
- 同时导入待测
useCounter
吊钩,如下图:
import useCounter from './useCounter'
- 现在我们可以编写第一个测试了。为了定义测试,我们使用 Jest 中的
test
函数。第一个参数是测试的名称,第二个参数是要作为测试运行的函数:
test('should use counter', () => {
- 在这个测试中,我们使用
renderHook
函数来定义我们的挂钩。此函数返回一个带有result
键的对象,该键将包含挂钩的结果:
const { result } = renderHook(() => useCounter())
- 现在我们可以使用 Jest 中的
expect
检查result
对象的值。result
对象包含一个current
键,该键将包含来自挂钩的当前结果:
expect(result.current.count).toBe(0)
expect(typeof result.current.increment).toBe('function')
})
正如我们所看到的,为挂钩结果编写测试非常简单!当创建自定义挂钩时,尤其是当它们将被公开使用时,我们应该始终编写测试以确保它们正常工作。
测试反钩动作
使用 React Hooks 测试库中的act
函数,我们可以执行 Hook 中的函数,然后检查新结果
现在让我们测试一下计数器挂钩的动作:
- 编写一个新的
test
函数,如下代码所示:
test('should increment counter', () => {
const { result } = renderHook(() => useCounter())
- 在
act
函数中调用挂钩的increment
函数:
act(() => result.current.increment())
- 最后,我们检查新的
count
现在是否为1
:
expect(result.current.count).toBe(1)
})
正如我们所看到的,我们可以简单地使用act
函数在挂钩中触发动作,然后像以前一样测试值。
测试 useCounter 初始值
我们还可以在调用act
前后检查结果,并将初始值传递给我们的挂钩。
现在让我们测试挂钩的初始值:
- 定义一个新的
test
函数,将初始值123
传递给挂钩:
test('should use initial value', () => {
const { result } = renderHook(() => useCounter(123))
- 现在我们可以检查
current
值是否等于初始值,调用increment
,并确保count
从初始值增加:
expect(result.current.count).toBe(123)
act(() => result.current.increment())
expect(result.current.count).toBe(124)
})
正如我们所看到的,我们可以简单地将初始值传递给挂钩并检查值是否相同。
测试重置和强制重新渲染
我们现在要模拟组件的道具变化。想象一下,我们的挂钩的初始值是一个道具,它最初是0
,之后会变为123
。如果我们现在重置计数器,它应该重置为123
而不是0
。但是,要做到这一点,我们需要在更改值后强制重新呈现测试组件。
现在让我们测试重置并强制组件重新渲染:
- 定义
test
函数和initial
值的变量:
test('should reset to initial value', () => {
let initial = 0
- 接下来,我们将呈现我们的挂钩,但这一次,我们还通过解构拉出了
rerender
函数:
const { result, rerender } = renderHook(() => useCounter(initial))
- 现在我们设置一个新的
initial
值并调用rerender
函数:
initial = 123
rerender()
- 我们的
initial
值现在应该已经改变了,所以当我们调用reset
时,count
将被设置为123
:
act(() => result.current.reset())
expect(result.current.count).toBe(123)
})
我们可以看到,测试库创建了一个虚拟组件,用于测试挂钩。我们可以强制这个虚拟组件重新渲染,以模拟当道具在真实组件中发生变化时会发生什么。
测试上下文挂钩
使用 React 挂钩测试库,我们还可以测试更复杂的挂钩,例如使用 React 上下文的挂钩。我们为博客应用创建的大多数定制挂钩都使用了上下文,所以我们现在要测试它们。为了测试使用上下文的挂钩,我们首先必须创建一个上下文包装器,然后才能测试挂钩。
在本节中,我们将执行以下操作:
- 创建一个
ThemeContextWrapper
组件 - 测试
useTheme
挂钩 - 创建一个
StateContextWrapper
组件 - 测试
useDispatch
挂钩 - 测试
useUserState
挂钩 - 测试
usePostsState
挂钩
让我们开始吧。
创建 ThemeSecondTextWrapper
为了能够测试主题挂钩,我们首先必须设置上下文并为挂钩的测试组件提供包装器组件。
现在我们创建ThemeContextWrapper
组件:
- 创建一个新的
src/hooks/testUtils.js
文件。 - 导入
React
和ThemeContext
,如下所示:
import React from 'react'
import { ThemeContext } from '../contexts'
- 定义一个名为
ThemeContextWrapper
的新功能组件;它将接受children
作为道具:
export function ThemeContextWrapper ({ children }) {
children
is a special prop of React components. It will contain all other components passed to it as children
; for example, <ThemeContextWrapper>{children}</ThemeContextWrapper>
.
- 我们返回一个带有默认主题的
ThemeContext.Provider
,然后将children
传递给它:
return (
<ThemeContext.Provider value={{ primaryColor: 'deepskyblue', secondaryColor: 'coral' }}>
{children}
</ThemeContext.Provider>
)
}
正如我们所看到的,上下文包装器只返回一个上下文提供程序组件。
测试 useTheme 挂钩
现在我们已经定义了ThemeContextWrapper
组件,我们可以在测试useTheme
挂钩时使用它。
现在让我们按照以下步骤测试useTheme
挂钩:
- 创建一个新的
src/hooks/useTheme.test.js
文件。 - 导入
renderHook
功能以及ThemeContextWrapper
和useTheme
挂钩:
import { renderHook } from '@testing-library/react-hooks'
import { ThemeContextWrapper } from './testUtils'
import useTheme from './useTheme'
- 接下来,使用
renderHook
函数定义test
,并将wrapper
作为第二个参数传递给它。这样做将使用定义的wrapper
组件包装测试组件,这意味着我们将能够在挂钩中使用提供的上下文:
test('should use theme', () => {
const { result } = renderHook(
() => useTheme(),
{ wrapper: ThemeContextWrapper }
)
- 现在我们可以检查挂钩的结果,它应该包含在
ThemeContextWrapper
中定义的颜色:
expect(result.current.primaryColor).toBe('deepskyblue')
expect(result.current.secondaryColor).toBe('coral')
正如我们所见,在提供了上下文包装器之后,我们可以测试使用上下文的挂钩,就像测试简单的计数器挂钩一样。
创建 StateContextWrapper
对于使用StateContext
的其他挂钩,我们必须定义另一个包装器来为挂钩提供StateContext
。
现在让我们通过以下步骤定义StateContextWrapper
组件:
- 编辑
src/hooks/testUtils.js
并调整导入语句,导入useReducer
挂钩、StateContext
和appReducer
功能:
import React, { useReducer } from 'react'
import { StateContext, ThemeContext } from '../contexts'
import appReducer from '../reducers'
- 定义一个名为
StateContextWrapper
的新功能组件。这里我们将使用useReducer
挂钩来定义应用状态,这与我们在src/App.js
文件中所做的类似:
export function StateContextWrapper ({ children }) {
const [ state, dispatch ] = useReducer(appReducer, { user: '', posts: [], error: '' })
- 接下来,定义并返回
StateContext.Provider
,这与我们对ThemeContextWrapper
所做的类似:
return (
<StateContext.Provider value={{ state, dispatch }}>
{children}
</StateContext.Provider>
)
}
正如我们所看到的,创建上下文包装器的工作方式总是类似的。然而,这一次,我们还在包装器组件中定义一个 Reducer 挂钩。
测试 useDispatch 挂钩
现在我们已经定义了StateContextWrapper
,我们可以用它来测试useDispatch
挂钩。
让我们按照以下步骤测试useDispatch
挂钩:
- 创建一个新的
src/hooks/useDispatch.test.js
文件。 - 导入
renderHook
功能、StateContextWrapper
组件和useDispatch
挂钩:
import { renderHook } from '@testing-library/react-hooks'
import { StateContextWrapper } from './testUtils'
import useDispatch from './useDispatch'
- 然后定义
test
函数,将StateContextWrapper
组件传递给它:
test('should use dispatch', () => {
const { result } = renderHook(
() => useDispatch(),
{ wrapper: StateContextWrapper }
)
- 最后,检查调度挂钩的结果是否为函数(
dispatch
函数):
expect(typeof result.current).toBe('function')
})
正如我们所看到的,使用wrapper
组件总是以同样的方式工作,即使我们在wrapper
组件中使用其他挂钩。
测试 useUserState 挂钩
使用StateContextWrapper
和调度挂钩,我们现在可以通过调度LOGIN
和REGISTER
动作并检查结果来测试useUserState
挂钩。为了分派这些操作,我们使用测试库中的act
函数。
让我们测试一下useUserState
挂钩:
- 创建一个新的
src/hooks/useUserState.test.js
文件。 - 导入必要的功能,
useDispatch
和useUserState
挂钩,以及StateContextWrapper
:
import { renderHook, act } from '@testing-library/react-hooks'
import { StateContextWrapper } from './testUtils'
import useDispatch from './useDispatch'
import useUserState from './useUserState'
- 接下来,我们编写一个检查初始
user
状态的test
:
test('should use user state', () => {
const { result } = renderHook(
() => useUserState(),
{ wrapper: StateContextWrapper }
)
expect(result.current).toBe('')
})
- 然后,我们编写一个
test
来发送一个LOGIN
动作,然后检查新状态。现在,我们不返回单个挂钩,而是返回一个包含两个挂钩结果的对象:
test('should update user state on login', () => {
const { result } = renderHook(
() => ({ state: useUserState(), dispatch: useDispatch() }),
{ wrapper: StateContextWrapper }
)
act(() => result.current.dispatch({ type: 'LOGIN', username: 'Test User' }))
expect(result.current.state).toBe('Test User')
})
- 最后,我们编写一个
test
来发送REGISTER
动作,然后检查新状态:
test('should update user state on register', () => {
const { result } = renderHook(
() => ({ state: useUserState(), dispatch: useDispatch() }),
{ wrapper: StateContextWrapper }
)
act(() => result.current.dispatch({ type: 'REGISTER', username: 'Test User' }))
expect(result.current.state).toBe('Test User')
})
正如我们所看到的,我们可以从测试中访问state
对象和dispatch
函数。
测试 usePostsState 挂钩
与我们测试useUserState
挂钩的方式类似,我们也可以测试usePostsState
挂钩。
现在让我们测试一下usePostsState
挂钩:
- 创建一个新的
src/hooks/usePostsState.test.js
文件。 - 导入必要的功能,
useDispatch
和usePostsState
挂钩,以及StateContextWrapper
:
import { renderHook, act } from '@testing-library/react-hooks'
import { StateContextWrapper } from './testUtils'
import useDispatch from './useDispatch'
import usePostsState from './usePostsState'
- 然后,我们
test
对posts
数组的初始状态:
test('should use posts state', () => {
const { result } = renderHook(
() => usePostsState(),
{ wrapper: StateContextWrapper }
)
expect(result.current).toEqual([])
})
- 接下来我们
test
一个FETCH_POSTS
动作是否替换了当前的posts
数组:
test('should update posts state on fetch action', () => {
const { result } = renderHook(
() => ({ state: usePostsState(), dispatch: useDispatch() }),
{ wrapper: StateContextWrapper }
)
const samplePosts = [{ id: 'test' }, { id: 'test2' }]
act(() => result.current.dispatch({ type: 'FETCH_POSTS', posts: samplePosts }))
expect(result.current.state).toEqual(samplePosts)
})
- 最后,我们
test
是否在CREATE_POST
动作中插入新帖子:
test('should update posts state on insert action', () => {
const { result } = renderHook(
() => ({ state: usePostsState(), dispatch: useDispatch() }),
{ wrapper: StateContextWrapper }
)
const post = { title: 'Hello World', content: 'This is a test', author: 'Test User' }
act(() => result.current.dispatch({ type: 'CREATE_POST', ...post }))
expect(result.current.state[0]).toEqual(post)
})
正如我们所看到的,posts
状态的测试与user
状态类似,但调度的动作不同。
测试异步挂钩
有时,我们需要测试执行异步操作的挂钩。这意味着我们需要等待一段时间,直到我们检查结果。为了实现对这类挂钩的测试,我们可以使用 React 挂钩测试库中的waitForNextUpdate
函数。
在测试异步挂钩之前,我们需要了解名为async
/await
的新 JavaScript 构造。
异步/等待构造
正常功能定义如下:
function doSomething () {
// ...
}
普通匿名函数定义如下:
() => {
// ...
}
通过添加async
关键字定义异步函数:
async function doSomething () {
// ...
}
我们还可以使匿名函数异步:
async () => {
// ...
}
在async
函数中,我们可以使用await
关键字解析承诺。我们不再需要执行以下操作:
() => {
fetchAPITodos()
.then(todos => dispatch({ type: FETCH_TODOS, todos }))
}
相反,我们现在可以这样做:
async () => {
const todos = await fetchAPITodos()
dispatch({ type: FETCH_TODOS, todos })
}
正如我们所看到的,async
函数使我们的代码更加简洁易读!现在我们已经了解了async
/await
构造,我们可以开始测试useDebouncedUndo
挂钩了。
测试使用的 BounceUndo 挂钩
我们将使用waitForNextUpdate
函数通过以下步骤在useDebouncedUndo
挂钩中测试去抖动:
- 创建一个新的
src/hooks/useDebouncedUndo.test.js
文件。 - 导入
renderHook
和act
功能以及useDebouncedUndo
挂钩:
import { renderHook, act } from '@testing-library/react-hooks'
import useDebouncedUndo from './useDebouncedUndo'
- 首先,我们
test
挂钩是否返回正确的result
,包括content
值、setter
函数和undoRest
对象:
test('should use debounced undo', () => {
const { result } = renderHook(() => useDebouncedUndo())
const [ content, setter, undoRest ] = result.current
expect(content).toBe('')
expect(typeof setter).toBe('function')
expect(typeof undoRest.undo).toBe('function')
expect(typeof undoRest.redo).toBe('function')
expect(undoRest.canUndo).toBe(false)
expect(undoRest.canRedo).toBe(false)
})
- 接下来,我们
test
是否立即更新content
值:
test('should update content immediately', () => {
const { result } = renderHook(() => useDebouncedUndo())
const [ content, setter ] = result.current
expect(content).toBe('')
act(() => setter('test'))
const [ newContent ] = result.current
expect(newContent).toBe('test')
})
Remember that we can give any name to variables we pull out from an array using destructuring. In this case, we first name the content
variable as content
, then, later, we name it newContent
.
- 最后,我们使用
waitForNextUpdate
等待去抖动效果触发。取消公告后,我们现在应该能够撤消更改:
test('should debounce undo history update', async () => {
const { result, waitForNextUpdate } = renderHook(() => useDebouncedUndo())
const [ , setter ] = result.current
act(() => setter('test'))
const [ , , undoRest ] = result.current
expect(undoRest.canUndo).toBe(false)
await act(async () => await waitForNextUpdate())
const [ , , newUndoRest ] = result.current
expect(newUndoRest.canUndo).toBe(true)
})
正如我们所看到的,我们可以使用async
/await
结合waitForNextUpdate
函数来轻松处理挂钩中的异步操作测试。
运行测试
要运行测试,只需执行以下命令:
> npm test
从以下屏幕截图可以看出,我们的所有测试都成功通过:
All Hook tests passing successfully
测试套件实际上监视文件中的更改,并自动重新运行测试。我们可以使用各种命令手动触发测试重新运行,我们可以按Q退出测试运行程序。
示例代码
示例代码可在Chapter10/chapter10_4
文件夹中找到。
只需运行npm install
安装所有依赖项,运行npm start
启动应用,然后在浏览器中访问http://localhost:3000
(如果它没有自动打开)。
探索 React 挂钩 API
官方 React 库提供了某些内置挂钩,可用于创建自定义挂钩。我们已经了解了 React 提供的三个基本挂钩:
useState
useEffect
useContext
此外,React 提供了更高级的挂钩,在某些用例中非常有用:
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
使用状态挂钩
useState
挂钩返回一个值,该值将在重新渲染时保持不变,并返回一个函数来更新该值。initialState
的值可以作为参数传递给它:
const [ state, setState ] = useState(initialState)
调用setState
更新值,并使用更新后的值重新呈现组件。如果该值没有更改,React 将不会重新渲染组件。
函数也可以传递给setState
函数,第一个参数是当前值。例如,考虑下面的代码:
setState(val => val + 1)
此外,如果初始状态是复杂计算的结果,则可以将函数传递给挂钩的第一个参数。在这种情况下,函数在挂钩初始化期间只调用一次:
const [ state, setState ] = useState(() => {
return computeInitialState()
})
状态挂钩是 React 提供的最基本、最普遍的挂钩。
使用效果挂钩
useEffect
挂钩接受一个函数,该函数包含带有副作用的代码,如计时器和订阅。传递给挂钩的函数将在渲染完成且组件出现在屏幕上后运行:
useEffect(() => {
// do something
})
可以从挂钩返回清理函数,该函数将在组件卸载时调用,用于清理计时器或订阅:
useEffect(() => {
const interval = setInterval(() => {}, 100)
return () => {
clearInterval(interval)
}
})
当效果的依赖项更新时,在再次触发效果之前也将调用 cleanup 函数。
为了避免在每次重新渲染时触发效果,我们可以指定一个值数组作为挂钩的第二个参数。只有当这些值中的任何一个发生更改时,才会再次触发该效果:
useEffect(() => {
// do something when state changes
}, [state])
作为第二个参数传递的数组称为效果的依赖项数组。如果您希望效果仅在装载期间触发,而 cleanup 函数在卸载期间触发,那么我们可以传递一个空数组作为第二个参数。
useContext 挂钩
useContext
挂钩接受一个上下文对象并返回上下文的当前value
。当上下文提供程序更新其value
时,挂钩将触发使用最新的value
重新呈现:
const value = useContext(NameOfTheContext)
需要注意的是,上下文对象本身需要传递给挂钩,而不是使用者或提供者。
useReducer 挂钩
useReducer
挂钩是useState
挂钩的高级版本。它接受一个reducer
作为第一个参数,这是一个有两个参数的函数:state
和action
。然后,reducer
函数返回根据当前状态和动作计算出的更新状态。如果简化器返回与前一状态相同的值,React 将不会重新渲染组件或触发效果:
const [ state, dispatch ] = useReducer(reducer, initialState, initFn)
在处理复杂的state
变化时,我们应该使用useReducer
挂钩而不是useState
挂钩。此外,处理全局state
更容易,因为我们可以简单地传递dispatch
函数,而不是多个 setter 函数。
The dispatch
function is stable and will not change on re-renders, so it is safe to omit it from useEffect
or the useCallback
dependencies.
我们可以通过设置initialState
值或指定initFn
函数作为第三个参数来指定初始state
。当计算初始state
需要很长时间时,或者当我们希望通过action
重新使用该函数重置state
时,指定这样的函数是有意义的。
使用备忘录挂钩
useMemo
挂钩获取函数的结果并将其存储。这意味着不会每次都重新计算它。此挂钩可用于性能优化:
const memoizedVal = useMemo(
() => computeVal(a, b, c),
[a, b, c]
)
在前面的示例中,computeVal
是一个性能要求很高的函数,它计算来自a
、b
和c
的结果。
useMemo
runs during rendering, so make sure the computation function does not cause any side effects, such as resource requests. Side effects should be put into a useEffect
Hook.
作为第二个参数传递的数组指定函数的依赖项。如果这些值中的任何一个发生变化,将重新计算该函数;否则,将使用存储的结果。如果未提供数组,则将在每次渲染时计算新值。如果传递空数组,则该值将只计算一次。
Do not rely on useMemo
to only compute things once. React may forget some previously memoized values if they are not used for a long time, for example, to free up memory. Use it only for performance optimizations.
useMemo
挂钩用于 React 组件的性能优化。
useCallback 挂钩
useCallback
吊钩的工作原理与useMemo
吊钩类似。但是,它返回一个已记忆的回调函数,而不是一个值:
const memoizedCallback = useCallback(
() => doSomething(a, b, c),
[a, b, c]
)
前面的代码类似于下面的useMemo
挂钩:
const memoizedCallback = useMemo(
() => () => doSomething(a, b, c),
[a, b, c]
)
只有在第二个参数数组中传递的依赖项值之一发生更改时,才会重新定义返回的函数。
useRef 挂钩
useRef
挂钩返回一个 ref 对象,该对象可以通过ref
属性分配给组件或元素。REF 可用于处理 React 中对图元和构件的引用:
const refContainer = useRef(initialValue)
将 ref 分配给元件或组件后,可通过refContainer.current
访问 ref。如果设置了InitialValue
,则在赋值前将refContainer.current
设置为该值。
以下示例定义了一个input
字段,该字段在渲染时将自动聚焦:
function AutoFocusField () {
const inputRef = useRef(null)
useEffect(() => inputRef.current.focus(), [])
return <input ref={inputRef} type="text" />
}
请务必注意,改变 ref 的当前值不会导致重新渲染。如果需要,我们应该使用useCallback
的ref
回调,如下所示:
function WidthMeasure () {
const [ width, setWidth ] = useState(0)
const measureRef = useCallback(node => {
if (node !== null) {
setWidth(node.getBoundingClientRect().width)
}
}, [])
return <div ref={measureRef}>I am {Math.round(width)}px wide</div>
}
REF 可用于访问 DOM,但也可用于保持可变值,例如存储对间隔的引用:
function Timer () {
const intervalRef = useRef(null)
useEffect(() => {
intervalRef.current = setInterval(doSomething, 100)
return () => clearInterval(intervalRef.current)
})
// ...
}
在前面的示例中使用 REF 使它们类似于类中的实例变量,例如this.intervalRef
。
使用命令手柄挂钩
useImperativeHandle
挂钩可用于定制实例值,当将ref
指向它时,这些实例值会暴露给其他组件。但是,应该尽可能避免这样做,因为它将组件紧密地耦合在一起,这会损害可重用性。
useImperativeHandle
挂钩具有以下签名:
useImperativeHandle(ref, createHandle, [dependencies])
例如,我们可以使用这个挂钩公开一个focus
函数,其他组件可以通过ref
向该组件触发该函数。此挂钩应与forwardRef
结合使用,如下所示:
function FocusableInput (props, ref) {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus()
}))
return <input {...props} ref={inputRef} />
}
FocusableInput = forwardRef(FocusableInput)
然后,我们可以访问focus
功能,如下所示:
function AutoFocus () {
const inputRef = useRef()
useEffect(() => inputRef.current.focus(), [])
return <FocusableInput ref={inputRef} />
}
正如我们所看到的,使用 refs 意味着我们可以直接访问元素和组件。
useLayoutEffect 挂钩
useLayoutEffect
挂钩与useEffect
挂钩相同,但它在所有 DOM 突变完成后以及在浏览器中呈现组件之前同步激发。它可以用于从 DOM 读取信息,并在渲染之前调整组件的外观。在浏览器渲染组件之前,将同步处理此挂钩内的更新。
除非确实需要,否则不要使用此挂钩,这仅适用于某些边缘情况。useLayoutEffect
将阻止浏览器中的视觉更新,因此速度比useEffect
慢。
这里的规则是先使用useEffect
。如果您的变异改变了 DOM 节点的外观,这可能导致它闪烁,那么您应该使用useLayoutEffect
。
usedbuggvalue 挂钩
useDebugValue
挂钩对于开发作为共享库一部分的自定义挂钩非常有用。它可用于显示某些值,以便在 React DevTools 中进行调试。
例如,在我们的useDebouncedUndo
定制挂钩中,我们可以执行以下操作:
export default function useDebouncedUndo (timeout = 200) {
const [ content, setInput ] = useState('')
const [ undoContent, { set: setContent, ...undoRest } ] = useUndo('')
useDebugValue('init')
const [ setDebounce, cancelDebounce ] = useDebouncedCallback(
(value) => {
setContent(value)
useDebugValue('added to history') },
timeout
)
useEffect(() => {
cancelDebounce()
setInput(undoContent.present)
useDebugValue(`waiting ${timeout}ms`)
}, [cancelDebounce, undoContent])
function setter (value) {
setInput(value)
setDebounce(value)
}
return [ content, setter, undoRest ]
}
添加这些useDebugValue
挂钩将在 React DevTools 中显示以下内容:
- 挂钩初始化时:debouncedudo:init
- 输入值时:取消公告撤消:等待 200 毫秒
- 去 Bouncing 后(在
200
ms 之后):去 BouncinedUndo:添加到历史记录中
总结
在本章中,我们首先学习了如何从博客应用中的现有代码中提取自定义挂钩。我们将各种上下文挂钩提取到定制挂钩中,然后创建 API 挂钩和一个更高级的挂钩来实现取消公告的撤销功能。接下来,我们学习了挂钩之间的交互,以及如何在自定义挂钩中使用来自其他挂钩的值。然后我们为我们的博客应用创建了本地挂钩。然后,我们学习了如何使用 Jest 和 React 挂钩测试库测试各种挂钩。最后,在撰写本文时,我们了解了 React Hooks API 提供的所有挂钩。
了解何时以及如何提取定制挂钩是 React 开发中一项非常重要的技能。在一个更大的项目中,我们可能会定义许多定制挂钩,专门针对我们项目的需求进行定制。自定义挂钩还可以使维护应用变得更容易,因为我们只需要在一个地方调整功能。测试定制挂钩非常重要,因为如果我们以后重构定制挂钩,我们希望确保它们仍然正常工作。现在我们了解了完整的 React 挂钩 API,我们可以利用 React 提供的所有挂钩来创建我们自己的定制挂钩。
在下一章中,我们将学习如何从 React 类组件迁移到基于挂钩的系统。我们将首先使用类组件创建一个小项目,然后使用挂钩将它们替换为函数组件,仔细观察两种解决方案之间的差异。
问题
为了总结本章所学内容,请尝试回答以下问题:
- 如何从现有代码中提取自定义挂钩?
- 创建 API 挂钩的优势是什么?
- 什么时候应该将功能提取到自定义挂钩中?
- 我们如何使用定制挂钩?
- 我们应该什么时候创建本地挂钩?
- 挂钩之间的哪些交互是可能的?
- 我们可以使用哪个库来测试挂钩?
- 我们如何测试挂钩动作?
- 我们如何测试上下文?
- 我们如何测试异步代码?
进一步阅读
如果您对本章所学概念的更多信息感兴趣,请阅读以下阅读材料:
- 创建自定义挂钩:https://reactjs.org/docs/hooks-custom.html
- React 挂钩测试库:https://react-hooks-testing-library.com/
- React 测试库y(用于测试组件):https://testing-library.com/react
- React 挂钩 API 参考:https://reactjs.org/docs/hooks-reference.html
- 何时使用
useCallback
:https://kentcdodds.com/blog/usememo-and-usecallback
版权属于:月萌API www.moonapi.com,转载请注明出处