React-挂钩学习手册-三-

59 阅读11分钟

React 挂钩学习手册(三)

原文:zh.annas-archive.org/md5/0d61b163bb6c28fa00edc962fdaa2667

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:使用 Hooks 进行路由

在上一章中,我们学习了如何使用 Hooks 请求资源。我们首先使用 State/Reducer 和 Effect Hooks 实现了请求资源,然后学习了axiosreact-request-hook库。

在本章中,我们将创建多个页面,并在我们的应用程序中实现路由。路由在几乎每个应用程序中都很重要。为了实现路由,我们将学习如何使用 Navi 库,这是一个基于 Hook 的导航系统。最后,我们还将学习动态链接以及如何使用 Hooks 访问路由信息。

本章将涵盖以下主题:

  • 创建多个页面

  • 实现路由

  • 使用路由 Hooks

技术要求

应该已经安装了相当新的 Node.js 版本(v11.12.0 或更高)。还需要安装 Node.js 的npm包管理器。

本章的代码可以在 GitHub 存储库上找到:github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter07.

查看以下视频以查看代码的运行情况:

bit.ly/2Mm9yoC

请注意,强烈建议您自己编写代码。不要简单地运行提供的代码示例。重要的是您自己编写代码,以便能够正确学习和理解。但是,如果遇到任何问题,您可以随时参考代码示例。

现在,让我们开始这一章。

创建多个页面

目前,我们的博客应用是所谓的单页面应用程序。然而,大多数较大的应用程序由多个页面组成。在博客应用中,我们至少希望为每篇博客文章创建一个单独的页面。

在设置路由之前,我们需要创建我们想要渲染的各种页面。在我们的博客应用中,我们将定义以下页面:

  • 主页将显示所有帖子的列表

  • 帖子页面,将显示单个帖子

所有页面都将显示HeaderBar,其中包括HeaderUserBarChangeThemeCreatePost组件。我们现在将开始创建HeaderBar组件。之后,我们将实现页面组件。

创建 HeaderBar 组件

首先,我们将重构App组件的一些内容到HeaderBar组件中。HeaderBar组件将包含我们想要在每个页面上显示的所有内容:HeaderUserBarChangeThemeCreatePost组件。

让我们开始创建HeaderBar组件:

  1. 创建一个新文件夹:src/pages/

  2. 创建一个新文件src/pages/HeaderBar.js,导入React(使用useContext钩子),并在那里定义组件。它将接受setTheme函数作为 prop:

import React, { useContext } from 'react'

export default function HeaderBar ({ setTheme }) {
   return (
        <div>
        </div>
    )
}
  1. 现在,从src/App.js组件中剪切以下代码,并将其插入到HeaderBar组件的<div>标签之间:
  <Header  text="React Hooks Blog" />  <ChangeTheme  theme={theme} setTheme={setTheme} /> <br /> <React.Suspense  fallback={"Loading..."}> <UserBar /> </React.Suspense> <br /> {user  && <CreatePost />} 
  1. 此外,从src/App.js中剪切以下导入语句(并调整路径),并将它们插入到src/pages/HeaderBar.js文件的开头,放在import React from 'react'语句之后:
import  CreatePost  from  '**../**post/CreatePost' import  UserBar  from  '**../**user/UserBar' import  Header  from  '**../**Header' import  ChangeTheme  from  '**../**ChangeTheme'
  1. 另外,导入ThemeContextStateContext
import { ThemeContext, StateContext } from '../contexts'
  1. 然后,在src/pages/HeaderBar.js中为themestate定义两个 Context Hooks,并从state对象中提取user变量,因为我们需要它进行条件检查,以确定是否应该渲染CreatePost组件:
export default function HeaderBar ({ setTheme }) { const theme = useContext(ThemeContext)

    const { state } = useContext(StateContext)
    const { user } = state 
    return (
  1. 现在,在src/App.js中导入HeaderBar组件:
import HeaderBar from './pages/HeaderBar'
  1. 最后,在src/App.js中渲染HeaderBar组件:
        <div style={{ padding: 8 }}>
            <HeaderBar setTheme={setTheme} />
            <hr />

现在,我们有一个独立的HeaderBar组件,它将显示在所有页面上。接下来,我们继续创建HomePage组件。

创建 HomePage 组件

现在,我们将从PostList组件和与帖子相关的 Resource Hook 中创建HomePage组件。同样,我们将重构src/App.js,以创建一个新的组件。

让我们开始创建HomePage组件:

  1. 创建一个新文件src/pages/HomePage.js,导入ReactuseEffectuseContext钩子,并在那里定义组件。我们还定义了一个 Context Hook,并提取了state对象和dispatch函数:
import React, { useEffect, useContext } from 'react'
import { StateContext } from '../contexts'

export default function HomePage () {
    const { state, dispatch } = useContext(StateContext)
    const { error } = state

    return (
        <div>
        </div>
    )
}
  1. 然后,从src/App.js中剪切以下导入语句(并调整路径),并在src/pages/HomePage.jsimport React from 'react'语句之后添加它们:
import  {  useResource  }  from  'react-request-hook'
import PostList from '**../**post/PostList'
  1. 接下来,从src/App.js中剪切以下 Hook 定义,并在HomePage函数的return语句之前插入它们:
 const  [  posts,  getPosts  ]  =  useResource(()  => ({ url:  '/posts', method:  'get' })) useEffect(getPosts, []) useEffect(()  =>  { if (posts  &&  posts.error) { dispatch({ type:  'POSTS_ERROR'  }) } if (posts  &&  posts.data) { dispatch({ type:  'FETCH_POSTS', posts:  posts.data.reverse() }) } }, [posts])
  1. 现在,从src/App.js中剪切以下渲染的代码,并将其插入到src/pages/HomePage.js<div>标签之间:
            {error && <b>{error}</b>}
            <PostList />
  1. 然后,在src/App.js中导入HomePage组件:
import HomePage from './pages/HomePage'
  1. 最后,在<hr />标签下方渲染HomePage组件:
            <hr />
            <HomePage />

现在,我们已经成功地将当前的代码重构为HomePage组件。接下来,我们将继续创建PostPage组件。

创建 PostPage 组件

现在,我们将定义一个新的页面组件,我们将从我们的 API 中仅获取单个帖子并显示它。

现在让我们开始创建PostPage组件:

  1. 创建一个新的src/pages/PostPage.js文件。

  2. 导入ReactuseEffectuseResource Hooks 以及Post组件:

import React, { useEffect } from 'react'
import { useResource } from 'react-request-hook'

import Post from '../post/Post'
  1. 现在,定义PostPage组件,它将接受帖子id作为 prop:
export default function PostPage ({ id }) {
  1. 在这里,我们定义了一个 Resource Hook,它将获取相应的post对象。我们将id作为依赖项传递给 Effect Hook,以便在id更改时重新获取我们的资源:
    const [ post, getPost ] = useResource(() => ({
        url: `/posts/${id}`,
        method: 'get'
    }))
    useEffect(getPost, [id])
  1. 最后,我们渲染Post组件:
    return (
        <div>
            {(post && post.data)
                ? <Post {...post.data} />
                : 'Loading...'
            }
            <hr />
        </div>
    )
}

现在我们也有了一个单独的页面用于单个帖子。

测试 PostPage

为了测试新页面,我们将在src/App.js中用PostPage组件替换HomePage组件,如下所示:

  1. src/App.js中导入PostPage组件:
import PostPage from './pages/PostPage'
  1. 现在,用PostPage组件替换HomePage组件:
            <PostPage id={'react-hooks'} />

正如我们所看到的,现在只有一个帖子,即 React Hooks 帖子,被渲染。

示例代码

示例代码可以在Chapter07/chapter7_1文件夹中找到。

只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

实现路由

我们将使用 Navi 库进行路由。Navi 原生支持 React Suspense、Hooks 和 React 的错误边界 API,这使得它非常适合通过 Hooks 实现路由。为了实现路由,我们首先要从上一节中定义的页面中定义路由。最后,我们将从主页面定义链接到相应的帖子页面,以及从这些页面返回到主页面。

在本章末尾,我们将通过实现路由 Hooks 来扩展我们的路由功能。

定义路由

在实现路由时的第一步是安装navireact-navi库。然后,我们定义路由。按照给定的步骤来做:

  1. 首先,我们必须使用npm安装这些库:
>npm install --save navi react-navi
  1. 然后,在src/App.js中,我们从 Navi 库导入RouterView组件以及mountroute函数:
import { Router, View } from 'react-navi'
import { mount, route } from 'navi'
  1. 确保导入了HomePage组件:
import HomePage from './pages/HomePage'
  1. 现在,我们可以使用mount函数来定义routes对象:
const routes = mount({
  1. 在这个函数中,我们定义了我们的路由,从主路由开始:
    '/': route({ view: <HomePage /> }),
  1. 接下来,我们定义单个帖子的路由,这里我们使用 URL 参数(:id),并且一个函数来动态创建view
    '/view/:id': route(req => {
        return { view: <PostPage id={req.params.id} /> }
    }),
})
  1. 最后,我们用<Router>组件包装我们渲染的代码,并用<View>组件替换<PostPage>组件,以便动态渲染当前页面:
 <Router routes={routes}>
            <div style={{ padding: 8 }}>
                <HeaderBar setTheme={setTheme} />
                <hr />
 <View />
            </div>
 </Router>

现在,如果我们去http://localhost:3000,我们可以看到所有帖子的列表,当我们去http://localhost:3000/view/react-hooks,我们可以看到一个单独的帖子:React Hooks 帖子。

定义链接

现在,我们将从每篇帖子定义链接到相应单独帖子的页面,然后从帖子页面返回到主页。这些链接将用于访问我们应用程序中定义的各种路由。首先,我们将从主页定义链接到单独的帖子页面。接下来,我们将从单独的帖子页面定义链接返回到主页。

定义到帖子的链接

我们首先在列表中缩短帖子的content,并且定义从PostList到相应帖子页面的链接。为此,我们必须在主页上从PostList定义静态链接到特定的帖子页面。

现在让我们定义这些链接:

  1. 编辑src/post/Post.js,并从react-navi导入Link组件:
import { Link } from 'react-navi'
  1. 接下来,我们将向Post组件添加两个新的 props:idshort,当我们想要显示帖子的缩短版本时,将其设置为true。稍后,我们将在PostList组件中将short设置为true
function Post ({ id, title, content, author, short = false }) {
  1. 接下来,当列出帖子时,我们将添加一些逻辑来将帖子的content修剪为30个字符:
    let processedContent = content
    if (short) {
        if (content.length > 30) {
            processedContent = content.substring(0, 30) + '...'
        }
    }
  1. 现在,我们可以显示processedContent值而不是content值,并且添加一个Link来查看完整的帖子:
            <div>{processedContent}</div>
 {short &&
 <div>
 <br />
 <Link href={`/view/${id}`}>View full post</Link>
 </div>
 }
  1. 最后,在PostList组件中将short属性设置为true。编辑src/post/PostList.js,并调整以下代码:
                <Post {...p} short={true} />

现在我们可以看到主页上的每篇帖子都被修剪为30个字符,并且有一个链接到相应的单独帖子页面:

在 PostList 中显示链接

正如我们所看到的,路由非常简单。现在,每篇帖子都有一个链接到其对应的完整帖子页面。

定义到主页的链接

现在,我们只需要一种方法从单个帖子页面返回到主页面。我们将重复类似的过程,就像我们之前所做的那样。现在让我们定义返回主页面的链接:

  1. 编辑src/pages/PostPage.js,并在那里导入Link组件:
import { Link } from 'react-navi'
  1. 然后,在显示帖子之前,插入一个返回主页面的新链接:
    return (
        <div>
            <div><Link href="/">Go back</Link></div>
  1. 进入页面后,我们现在可以使用返回链接返回到主页面:

在单个帖子页面上显示链接

现在,我们的应用程序还提供了返回主页的方法。

调整 CREATE_POST 动作

以前,当创建新帖子时,我们会调度CREATE_POST动作。但是,此操作不包含帖子id,这意味着对新创建的帖子的链接将无法工作。

我们现在要调整代码,将帖子id传递给CREATE_POST动作:

  1. 编辑src/post/CreatePost.js,并导入useEffect Hook:
import React, { useState, useContext, useEffect } from 'react'
  1. 接下来,调整现有的 Resource Hook,在创建帖子完成后提取post对象:
    const [ post, createPost ] = useResource(({ title, content, author }) => ({
  1. 现在,我们可以在 Resource Hook 之后创建一个新的 Effect Hook,并在创建帖子请求的结果可用时调度CREATE_POST动作:
    useEffect(() => {
        if (post && post.data) {
            dispatch({ type: 'CREATE_POST', ...post.data })
        }
    }, [post])
  1. 接下来,我们在handleCreate处理程序函数中删除对dispatch函数的调用:
    function handleCreate () {
        createPost({ title, content, author: user })
 dispatch({ type: 'CREATE_POST', title, content, author: user })
    }
  1. 最后,我们编辑src/reducers.js,并调整postsReducer如下:
function postsReducer (state, action) {
    switch (action.type) {
        case 'FETCH_POSTS':
            return action.posts

        case 'CREATE_POST':
            const newPost = { title: action.title, content: action.content, author: action.author, id: action.id }
            return [ newPost, ...state ]

现在,对新创建的帖子的链接正常工作,因为id值已添加到插入的post对象中。

示例代码

示例代码可以在Chapter07/chapter7_2文件夹中找到。

只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

使用路由钩子

在使用navireact-navi实现基本路由之后,我们现在将使用路由钩子来实现更高级的用例,这些路由钩子由react-navi提供。路由钩子可用于使路由更加动态。例如,通过允许从其他 Hooks 导航到不同的路由。此外,我们可以使用 Hooks 在组件内访问所有与路由相关的信息。

Navi 的 Hooks 概述

首先,我们将看一下 Navi 库提供的三个 Hooks:

  • useNavigation钩子

  • useCurrentRoute钩子

  • useLoadingRoute钩子

useNavigation Hook

useNavigation钩子具有以下签名:

const navigation = useNavigation()

它返回 Navi 的navigation对象,其中包含以下函数来管理应用程序的导航状态:

  • extractState():返回window.history.state的当前值;在处理服务器端渲染时很有用。

  • getCurrentValue(): 返回与当前 URL 对应的Route对象。

  • getRoute():返回一个 promise,该 promise 对应于当前 URL 的完全加载的Route对象。只有在Route对象完全加载后才会解析该 promise。

  • goBack(): 返回上一页;这类似于按下浏览器返回按钮的操作。

  • navigate(url, options): 使用提供的选项(body, headers, method, replace, 和 state)导航到提供的 URL。有关选项的更多信息可以在官方 Navi 文档中找到:frontarm.com/navi/en/reference/navigation/#navigationnavigate.

useCurrentRoute Hook

useCurrentRoute Hook 具有以下签名:

const route = useCurrentRoute()

它返回最新的非忙碌路由,其中包含 Navi 对当前页面的所有了解:

  • 数据:包含所有data块的合并值。

  • 标题:包含应设置在document.title上的title值。

  • url: 包含有关当前路由的信息,例如hrefqueryhash

  • 视图:包含将在路由视图中呈现的组件或元素的数组。

useLoadingRoute Hook

useLoadingRoute Hook 具有以下签名:

const loadingRoute = useLoadingRoute()

它返回当前正在获取的页面的Route对象。如果当前没有获取页面,则输出undefined。该对象与useCurrentRoute Hook 的Route对象看起来相同。

程序化导航

首先,我们将使用useNavigation Hook 来实现程序化导航。我们希望在创建新帖子后自动重定向到相应的帖子页面。

让我们使用 Hooks 在CreatePost组件中实现程序化导航:

  1. 编辑src/post/CreatePost.js,并在那里导入useNavigation Hook:
import { useNavigation } from 'react-navi'
  1. 现在,在现有的 Resource Hook 之后定义一个 Navigation Hook:
    const navigation = useNavigation()
  1. 最后,我们调整 Effect Hook 以调用navigation.navigate(),一旦创建帖子请求的结果可用:
    useEffect(() => {
        if (post && post.data) {
            dispatch({ type: 'CREATE_POST', ...post.data })
            navigation.navigate(`/view/${post.data.id}`)
        }
    }, [post])

如果我们现在创建一个新的post对象,我们会发现在按下创建按钮后,我们会自动被重定向到相应帖子的页面。现在我们可以继续使用 Hooks 来访问路由信息。

访问路由信息

接下来,我们将使用useCurrentRoute Hook 来访问有关当前路由/URL 的信息。我们将使用此 Hook 来实现一个页脚,它将显示当前路由的href值。

让我们开始实现页脚:

  1. 首先,我们为页脚创建一个新组件。创建一个新的src/pages/FooterBar.js文件,并从react-navi中导入React以及useCurrentRoute Hook:
import React from 'react'
import { useCurrentRoute } from 'react-navi'
  1. 然后,我们定义一个新的FooterBar组件:
export default function FooterBar () {
  1. 我们使用useCurrentRoute Hook,并提取url对象以便在页脚中显示当前的href值:
    const { url } = useCurrentRoute()
  1. 最后,在页脚中呈现当前href值的链接:
    return (
        <div>
            <a href={url.href}>{url.href}</a>
        </div>
    )
}

现在,当我们打开一个帖子页面时,我们可以在页脚中看到当前帖子的href值:

显示当前 href 值的页脚

正如我们所看到的,我们的页脚正常工作——它始终显示当前页面的href值。

示例代码

示例代码可以在Chapter07/chapter7_3文件夹中找到。

只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

总结

在本章中,我们首先为我们的博客定义了两个页面:主页和单个帖子页面。我们还为HeaderBar创建了一个组件。之后,我们通过定义路由、链接到单个帖子以及返回主页的链接来实现了路由。最后,我们在创建新帖子时使用路由 Hooks 来实现动态导航,并实现了一个显示当前 URL 的页脚。

路由非常重要,在几乎每个应用程序中都会使用。我们现在知道如何定义单独的页面以及如何在它们之间进行链接。此外,我们学会了如何使用 Hooks 在页面之间进行动态导航。我们还学会了如何使用 Hooks 访问路由信息以进行更高级的用例。

Navi 库可以做很多其他事情。但是,本书侧重于 Hooks,因此大多数 Navi 的功能都不在讨论范围之内。例如,我们可以使用 Navi 获取数据,实现错误页面(例如 404 页面),延迟加载和组合路由。请随意阅读官方 Navi 文档中关于这些功能的内容。

在下一章中,我们将学习由 React 社区提供的各种 Hooks:用于输入处理、响应式设计、实现撤销/重做,以及使用 Hooks 实现各种数据结构和 React 生命周期方法。我们还将学习在哪里可以找到社区提供的更多 Hooks。

问题

为了回顾本章学到的内容,请尝试回答以下问题:

  1. 为什么我们需要定义单独的页面?

  2. 我们如何使用 Navi 库定义路由?

  3. 我们如何使用 URL 参数定义路由?

  4. 如何使用 Navi 定义静态链接?

  5. 我们如何实现动态导航?

  6. 哪个 Hook 用于访问当前路由的路由信息?

  7. 哪个 Hook 用于访问当前加载路由的路由信息?

进一步阅读

如果您对本章学到的概念感兴趣,可以查看 Navi 库的官方文档:frontarm.com/navi/en/

第八章:使用社区 Hooks

在上一章中,我们使用 Navi 库实现了路由。我们首先实现了页面,然后定义了路由和静态链接。最后,我们实现了动态链接,并使用 Hooks 访问了路由信息。

在本章中,我们将学习由 React 社区提供的各种 Hooks。这些 Hooks 可以用于简化输入处理,并实现 React 生命周期,以简化从 React 类组件迁移。此外,还有一些实现各种行为的 Hooks,例如定时器、检查客户端是否在线、悬停和焦点事件以及数据操作。最后,我们将学习响应式设计,并使用 Hooks 实现撤销/重做功能。

本章将涵盖以下主题:

  • 使用 Input Hook 简化输入处理

  • 使用 Hooks 实现 React 生命周期

  • 学习各种有用的 Hooks(usePrevious、定时器、在线、焦点、悬停和数据操作 Hooks)

  • 使用 Hooks 实现响应式设计

  • 使用 Hooks 实现撤销/重做功能和去抖动

  • 学习在哪里找到其他的 Hooks

技术要求

应该已经安装了一个相当新的 Node.js 版本(v11.12.0 或更高)。还需要安装 Node.js 的npm包管理器。

本章的代码可以在 GitHub 存储库中找到:github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter08.

查看以下视频以查看代码的运行情况:

bit.ly/2Mm9yoC

请注意,强烈建议您自己编写代码。不要简单地运行提供的代码示例。重要的是您自己编写代码,以便能够正确地学习和理解。但是,如果遇到任何问题,您可以随时参考代码示例。

现在,让我们开始这一章。

探索输入处理 Hook

在处理 Hooks 时,一个非常常见的用例是使用 State 和 Effect Hooks 存储input字段的当前值。在本书中,我们已经多次这样做了。

useInput Hook 极大地简化了这种用例,通过提供一个处理input字段的value变量的单个 Hook。它的工作方式如下:

import React from 'react'
import { useInput } from 'react-hookedup'

export default function App () {
    const { value, onChange } = useInput('')

    return <input value={value} onChange={onChange} />
}

这段代码将绑定一个onChange处理函数和valueinput字段。这意味着每当我们在input字段中输入文本时,value将自动更新。

另外,还有一个函数可以清除input字段。这个clear函数也是从 Hook 中返回的:

    const { clear } = useInput('')

调用clear函数将把value设置为空值,并清除input字段中的所有文本。

此外,该 Hook 提供了两种绑定input字段的方式:

  • bindToInput:将valueonChange属性绑定到input字段,使用e.target.value作为onChange函数的value参数。在处理 HTMLinput字段时非常有用。

  • bind:将valueonChange属性绑定到input字段,仅使用e作为onChange函数的值。这对于直接将值传递给onChange函数的 React 组件非常有用。

bindbindToInput对象可以与扩展运算符一起使用,如下所示:

import React from 'react'
import { useInput } from 'react-hookedup'

const ToggleButton = ({ value, onChange }) => { ... } // custom component that renders a toggle button

export default function App () {
    const { bind, bindToInput } = useInput('')

    return (
        <div>
            <input {...bindToInput} />
            <ToggleButton {...bind} />
        </div>
    )
}

正如我们所看到的,对于input字段,我们可以使用{...bindToInput}属性来分配valueonChange函数。对于ToggleButton,我们需要使用{...bind}属性,因为这里我们不处理输入事件,并且值直接传递给 change 处理程序(而不是通过e.target.value)。

现在我们已经了解了 Input Hook,我们可以继续在我们的博客应用中实现它。

在我们的博客应用中实现 Input Hooks

现在我们已经了解了 Input Hook,以及它如何简化处理input字段状态,我们将在我们的博客应用中实现 Input Hooks。

首先,我们必须在我们的博客应用项目中安装react-hookedup库。

> npm install --save react-hookedup

我们现在将在以下组件中实现 Input Hooks:

  • Login组件

  • Register组件

  • CreatePost组件

让我们开始实现 Input Hooks。

Login组件

Login组件中有两个input字段:用户名和密码字段。我们现在将用 Input Hooks 替换 State Hooks。

现在让我们开始在Login组件中实现 Input Hooks:

  1. src/user/Login.js文件的开头导入useInput Hook:
import { useInput } from 'react-hookedup'
  1. 然后,我们移除以下username State Hook:
    const [ username, setUsername ] = useState('')

它被替换为 Input Hook,如下所示:

    const { value: username, bindToInput: bindUsername } = useInput('')

由于我们使用了两个输入钩子,为了避免名称冲突,我们在对象解构中使用重命名语法({ from: to })将value键重命名为username,将bindToInput键重命名为bindUsername

  1. 我们还移除以下password状态钩子:
    const [ password, setPassword ] = useState('')

它被输入钩子替换,如下所示:

    const { value: password, bindToInput: bindPassword } = useInput('')
  1. 现在我们可以移除以下处理函数:
    function handleUsername (evt) {
        setUsername(evt.target.value)
    }

    function handlePassword (evt) {
        setPassword(evt.target.value)
    }
  1. 最后,我们不再手动传递onChange处理程序,而是使用输入钩子中的绑定对象:
            <input type="text" value={username} {...bindUsername} name="login-username" id="login-username" />
            <input type="password" value={password} {...bindPassword} name="login-password" id="login-password" />

登录功能仍然与以前完全相同,但现在我们使用更简洁的输入钩子,而不是通用状态钩子。我们也不再需要为每个input字段定义相同类型的处理函数。正如我们所看到的,使用社区钩子可以极大地简化常见用例的实现,比如输入处理。现在我们将重复相同的过程用于Register组件。

注册组件

Register组件的工作方式类似于Login组件。但是,它有三个input字段:用户名、密码和重复密码。

现在让我们在Register组件中实现输入钩子:

  1. src/user/Register.js文件的开头导入useInput钩子:
import { useInput } from 'react-hookedup'
  1. 然后,我们移除以下状态钩子:
    const [ username, setUsername ] = useState('')
    const [ password, setPassword ] = useState('')
    const [ passwordRepeat, setPasswordRepeat ] = useState('')

它们被相应的输入钩子替换:

    const { value: username, bindToInput: bindUsername } = useInput('')
    const { value: password, bindToInput: bindPassword } = useInput('')
    const { value: passwordRepeat, bindToInput: bindPasswordRepeat } = useInput('')
  1. 同样,我们可以移除所有处理函数:
 function  handleUsername  (evt)  { setUsername(evt.target.value)
 } function  handlePassword  (evt)  { setPassword(evt.target.value)
 } function  handlePasswordRepeat  (evt)  { setPasswordRepeat(evt.target.value)
 }
  1. 最后,我们用相应的绑定对象替换所有的onChange处理程序:
 <input  type="text"  value={username} **{...bindUsername****}** name="register-username" id="register-username" /> <input  type="password"  value={password} **{...bindPassword****}** name="register-password" id="register-password" /> <input  type="password"  value={passwordRepeat} **{...bindPasswordRepeat}** name="register-password-repeat" id="register-password-repeat/>

注册功能仍然以相同的方式工作,但现在使用输入钩子。接下来是CreatePost组件,我们也将在其中实现输入钩子。

创建帖子组件

CreatePost组件使用两个input字段:一个用于title,一个用于content。我们将用输入钩子替换它们。

现在让我们在CreatePost组件中实现输入钩子:

  1. src/user/CreatePost.js文件的开头导入useInput钩子:
import { useInput } from 'react-hookedup'
  1. 然后,我们移除以下状态钩子:
    const [ title, setTitle ] = useState('')
    const [ content, setContent ] = useState('')

我们用相应的输入钩子替换它们:

    const { value: title, bindToInput: bindTitle } = useInput('')
    const { value: content, bindToInput: bindContent } = useInput('')
  1. 同样,我们可以移除以下输入处理函数:
 function  handleTitle  (evt)  { setTitle(evt.target.value)
 } function  handleContent  (evt)  { setContent(evt.target.value)
 }
  1. 最后,我们用相应的绑定对象替换所有的onChange处理程序:
 <input  type="text"  value={title} **{...bindTitle}** name="create-title" id="create-title" />
        </div>
 <textarea  value={content} **{...bindContent}** />

创建帖子功能也将以相同的方式与输入钩子一起工作。

示例代码

示例代码可以在Chapter08/chapter8_1文件夹中找到。

只需运行 npm install 以安装所有依赖项,然后运行 npm start 启动应用程序,然后在浏览器中访问 localhost:3000 (如果没有自动打开)。

React 生命周期与 Hooks

正如我们在之前的章节中学到的,我们可以使用 useEffect Hook 来模拟大部分 React 的生命周期方法。然而,如果你更喜欢直接处理 React 生命周期,而不是使用 Effect Hooks,有一个名为 react-hookedup 的库,它提供了各种 Hooks,包括各种 React 生命周期的 Hooks。此外,该库还提供了一个合并状态的 Hook,它的工作方式类似于 React 类组件中的 this.setState()

useOnMount Hook

useOnMount Hook 与 componentDidMount 生命周期有类似的效果。它的使用方法如下:

import React from 'react'
import { useOnMount } from 'react-hookedup'

export default function UseOnMount () {
    useOnMount(() => console.log('mounted'))

    return <div>look at the console :)</div>
}

当组件挂载时(当 React 组件首次渲染时),上述代码将在控制台输出 mounted。例如,由于 prop 更改而导致组件重新渲染时,它不会再次被调用。

或者,我们可以使用带有空数组作为第二个参数的 useEffect Hook,它将产生相同的效果:

import React, { useEffect } from 'react'

export default function OnMountWithEffect () {
    useEffect(() => console.log('mounted with effect'), [])

    return <div>look at the console :)</div>
}

正如我们所看到的,使用带有空数组作为第二个参数的 Effect Hook 会产生与 useOnMount Hook 或 componentDidMount 生命周期方法相同的行为。

useOnUnmount Hook

useOnUnmount Hook 与 componentWillUnmount 生命周期有类似的效果。它的使用方法如下:

import React from 'react'
import { useOnUnmount } from 'react-hookedup'

export default function UseOnUnmount () {
    useOnUnmount(() => console.log('unmounting'))

    return <div>click the "unmount" button above and look at the console</div>
}

当组件卸载时(在 React 组件从 DOM 中移除之前),上述代码将在控制台输出 unmounting。

如果你还记得第四章中所学到的,我们可以从 useEffect Hook 中返回一个清理函数,当组件卸载时将被调用。这意味着我们可以使用 useEffect 来实现 useOnMount Hook,如下所示:

import React, { useEffect } from 'react'

export default function OnUnmountWithEffect () {
    useEffect(() => {
        return () => console.log('unmounting with effect')
    }, [])

    return <div>click the "unmount" button above and look at the console</div>
}

正如我们所看到的,从 Effect Hook 返回的清理函数,带有空数组作为第二个参数,具有与 useOnUnmount Hook 或 componentWillUnmount 生命周期方法相同的效果。

useLifecycleHooks Hook

useLifecycleHooks Hook 将前两个 Hook 结合为一个。我们可以将 useOnMount 和 useOnUnmount Hooks 结合如下:

import React from 'react'
import { useLifecycleHooks } from 'react-hookedup'

export default function UseLifecycleHooks () {
    useLifecycleHooks({
        onMount: () => console.log('lifecycle mounted'),
        onUnmount: () => console.log('lifecycle unmounting')
    })

    return <div>look at the console and click the button</div>
}

或者,我们可以分别使用这两个 Hooks:

import React from 'react'
import { useOnMount, useOnUnmount } from 'react-hookedup'

export default function UseLifecycleHooksSeparate () {
    useOnMount(() => console.log('separate lifecycle mounted'))
    useOnUnmount(() => console.log('separate lifecycle unmounting'))

    return <div>look at the console and click the button</div>
}

然而,如果你有这种模式,我建议简单地使用useEffect Hook,如下所示:

import React, { useEffect } from 'react'

export default function LifecycleHooksWithEffect () {
    useEffect(() => {
        console.log('lifecycle mounted with effect')
        return () => console.log('lifecycle unmounting with effect')
    }, [])

    return <div>look at the console and click the button</div>
}

使用useEffect,我们可以将整个效果放入一个函数中,然后简单地返回一个清理函数。当我们在下一章学习如何制作自己的 Hooks 时,这种模式尤其有用。

效果让我们以不同的方式思考 React 组件。我们根本不必考虑组件的生命周期。相反,我们考虑效果、依赖关系和效果的清理。

useMergeState Hook

useMergeState Hook 的工作方式类似于useState Hook。但是,它不会替换当前状态,而是将当前状态与新状态合并,就像在 React 类组件中的this.setState()一样。

Merge State Hook 返回以下对象:

  • state:当前状态

  • setState:一个函数,用于将当前状态与给定的状态对象合并

例如,让我们考虑以下组件:

  1. 首先,我们导入useState Hook:
import React, { useState } from 'react'
  1. 然后,我们定义我们的应用组件和一个包含loaded值和counter值的对象的 State Hook:
export default function MergeState () {
    const [ state, setState ] = useState({ loaded: true, counter: 0 })
  1. 接下来,我们定义一个handleClick函数,在其中设置新的state,将当前的counter值增加1
    function handleClick () {
        setState({ counter: state.counter + 1 })
    }
  1. 最后,我们渲染当前的counter值和一个+1 按钮,以便将counter值增加1。如果state.loadedfalseundefined,按钮将被禁用:
    return (
        <div>
            Count: {state.counter}
            <button onClick={handleClick} disabled={!state.loaded}>+1</button>
        </div>
    )
}

正如我们所看到的,我们有一个简单的计数器应用,显示当前计数和一个+1 按钮。只有当loaded值设置为true时,+1 按钮才会启用。

如果我们现在点击+1 按钮,counter将从0增加到1,但按钮将被禁用,因为我们已经用新的state对象覆盖了当前的state对象。

为了解决这个问题,我们需要调整handleClick函数如下:

    function handleClick () {
        setState({ ...state, counter: state.counter + 1 })
    }

或者,我们可以使用useMergeState Hook,以避免这个问题,并获得与在类组件中使用this.setState()相同的行为:

import React from 'react'
import { useMergeState } from 'react-hookedup'

export default function UseMergeState () {
    const { state, setState } = useMergeState({ loaded: true, counter: 0 })

正如我们所看到的,通过使用useMergeState Hook,我们可以复制在类组件中使用this.setState()时的相同行为。因此,我们不再需要使用扩展语法。然而,通常最好简单地使用多个 State Hooks 或 Reducer Hook。

示例代码

示例代码可以在Chapter08/chapter8_2文件夹中找到。

只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

各种有用的 Hooks

除了生命周期 Hooks 之外,react-hookedup还提供了用于计时器、检查网络状态以及处理其他有用的 Hooks,例如数组和输入字段的 Hooks。我们现在将介绍react-hookedup提供的其余 Hooks。

这些 Hooks 如下:

  • usePrevious Hook,用于获取 Hook 或 prop 的先前值

  • 计时器 Hooks,用于实现间隔和超时

  • useOnline Hook,用于检查客户端是否有活动的互联网连接

  • 用于处理布尔值、数组和计数器的各种数据操作 Hooks

  • 处理焦点和悬停事件的 Hooks

usePrevious Hook

usePrevious Hook 是一个简单的 Hook,让我们获取 prop 或 Hook 值的先前值。它将始终存储并返回任何给定变量的先前值,并且工作方式如下:

  1. 首先,我们导入useStateusePrevious Hooks:
import React, { useState } from 'react'
import { usePrevious } from 'react-hookedup'
  1. 然后,我们定义我们的App组件,并在其中存储当前count状态的 Hook:
export default function UsePrevious () {
    const [ count, setCount ] = useState(0)
  1. 现在,我们定义usePrevious Hook,将 State Hook 中的count值传递给它:
    const prevCount = usePrevious(count)

usePrevious Hook 适用于任何变量,包括组件 props 和其他 Hooks 的值。

  1. 接下来,我们定义一个处理函数,它将通过1增加count
    function handleClick () {
        setCount(count + 1)
    }
  1. 最后,我们渲染count的先前值,count的当前值以及一个增加count的按钮:
    return (
        <div>
            Count was {prevCount} and is {count} now.
            <button onClick={handleClick}>+1</button>
        </div>
    )
}

先前定义的组件将首先显示 Count was and is 0 now.,因为 Previous Hook 的默认值是null。单击按钮一次后,将显示以下内容:Count was 0 and is 1 now.。

计时器 Hooks

react-hookedup库还提供了用于处理计时器的 Hooks。如果我们在组件中简单地使用setTimeoutsetInterval创建计时器,那么每次组件重新渲染时都会重新实例化。这不仅会导致错误和不可预测性,而且如果旧的计时器没有正确释放,还可能导致内存泄漏。使用计时器 Hooks,我们可以完全避免这些问题,并轻松地使用间隔和超时。

该库提供以下计时器 Hooks:

  • useInterval Hook,用于在 React 组件中定义setInterval计时器(多次触发的计时器)

  • useTimeout Hook 用于定义setTimeout定时器(在一定时间后仅触发一次的定时器)

useInterval Hook

useInterval Hook 可以像setInterval一样使用。我们现在将实现一个小计数器,用于计算自组件挂载以来的秒数:

  1. 首先,导入useStateuseInterval Hooks:
import React, { useState } from 'react'
import { useInterval } from 'react-hookedup'
  1. 然后,我们定义我们的组件和一个 State Hook:
export default function UseInterval () {
    const [ count, setCount ] = useState(0)
  1. 接下来,我们定义useInterval Hook,它将每 1000 毫秒增加1,相当于1秒:
    useInterval(() => setCount(count + 1), 1000)
  1. 最后,我们显示当前的count值:
    return <div>{count} seconds passed</div>
}

或者,我们可以使用 Effect Hook 与setInterval结合,而不是useInterval Hook,如下所示:

import React, { useState, useEffect } from 'react'

export default function IntervalWithEffect () {
    const [ count, setCount ] = useState(0)
    useEffect(() => {
        const interval = setInterval(() => setCount(count + 1), 1000)
        return () => clearInterval(interval)
    })

    return <div>{count} seconds passed</div>
}

正如我们所看到的,useInterval Hook 使我们的代码更加简洁和易读。

useTimeout Hook

useTimeout Hook 可以像setTimeout一样使用。现在我们将实现一个在经过10秒后触发的组件:

  1. 首先,导入useStateuseTimeout Hooks:
import React, { useState } from 'react'
import { useTimeout } from 'react-hookedup'
  1. 然后,我们定义我们的组件和一个 State Hook:
export default function UseTimeout () {
    const [ ready, setReady ] = useState(false)
  1. 接下来,我们定义useTimeout Hook,它将在10000毫秒(10秒)后将ready设置为true
    useTimeout(() => setReady(true), 10000)
  1. 最后,我们显示我们是否准备好了:
    return <div>{ready ? 'ready' : 'waiting...'}</div>
}

或者,我们可以使用 Effect Hook 与setTimeout结合,而不是useTimeout Hook,如下所示:

import React, { useState, useEffect } from 'react'

export default function TimeoutWithEffect () {
    const [ ready, setReady ] = useState(false)
    useEffect(() => {
        const timeout = setTimeout(() => setReady(true), 10000)
        return () => clearTimeout(timeout)
    })

    return <div>{ready ? 'ready' : 'waiting...'}</div>
}

正如我们所看到的,useTimeout Hook 使我们的代码更加简洁和易读。

在线状态 Hook

在一些 Web 应用中,实现离线模式是有意义的;例如,如果我们希望能够在本地编辑和保存帖子草稿,并在再次在线时将它们同步到服务器。为了实现这种用例,我们可以使用useOnlineStatus Hook。

在线状态 Hook 返回一个带有online值的对象,如果客户端在线则包含true;否则包含false。它的工作原理如下:

import React from 'react'
import { useOnlineStatus } from 'react-hookedup'

export default function App () {
    const { online } = useOnlineStatus()

    return <div>You are {online ? 'online' : 'offline'}!</div>
}

前面的组件将在有网络连接时显示“您在线!”,否则显示“您离线!”。

然后,我们可以使用 Previous Hook,结合 Effect Hook,以便在我们再次在线时将数据同步到服务器:

import React, { useEffect } from 'react'
import { useOnlineStatus, usePrevious } from 'react-hookedup'

export default function App () {
    const { online } = useOnlineStatus()
    const prevOnline = usePrevious(online)

    useEffect(() => {
        if (prevOnline === false && online === true) {
            alert('syncing data')
        }
    }, [prevOnline, online])

    return <div>You are {online ? 'online' : 'offline'}!</div>
}

现在,我们有一个 Effect Hook,每当online的值发生变化时触发。然后它检查先前的online值是否为false,当前值是否为true。如果是这种情况,这意味着我们先前是离线的,现在又在线了,所以我们需要将更新的数据同步到服务器。

因此,当我们离线然后再次在线时,我们的应用将显示一个显示同步数据的警报。

数据操作 Hook

react-hookedup库提供了处理数据的各种实用 Hook。这些 Hook 简化了处理常见数据结构,并提供了对 State Hook 的抽象。

提供了以下数据操作 Hook:

  • useBoolean Hook:处理切换布尔值

  • useArray Hook:处理数组

  • useCounter Hook:处理计数器

useBoolean Hook

useBoolean Hook 用于处理切换布尔值(true/false),并提供了将值设置为true/false的函数,以及一个toggle函数来切换值。

该 Hook 返回一个具有以下内容的对象:

  • value:布尔值的当前值

  • toggle:一个用于切换当前值的函数(如果当前为false,则设置为true,如果当前为true,则设置为false

  • setTrue:将当前值设置为true

  • setFalse:将当前值设置为false

布尔值 Hook 的工作方式如下:

  1. 首先,我们从react-hookedup中导入useBoolean Hook:
import React from 'react'
import { useBoolean } from 'react-hookedup'
  1. 然后,我们定义我们的组件和布尔值 Hook,它返回一个具有toggle函数和value的对象。我们将false作为默认值传递:
export default function UseBoolean () {
    const { toggle, value } = useBoolean(false)
  1. 最后,我们渲染一个按钮,可以打开/关闭:
    return (
        <div>
            <button onClick={toggle}>{value ? 'on' : 'off'}</button>
        </div>
    )
}

按钮最初将以文本“关闭”呈现。单击按钮时,它将显示文本“打开”。再次单击时,它将再次关闭。

useArray Hook

useArray Hook 用于轻松处理数组,而无需使用其余/扩展语法。

Array Hook 返回一个具有以下内容的对象:

  • value:当前数组

  • setValue:将新数组设置为值

  • add:将给定元素添加到数组中

  • clear:从数组中移除所有元素

  • removeIndex:通过索引从数组中移除元素

  • removeById:通过其id(假设数组中的元素是具有id键的对象)从数组中移除元素

它的工作方式如下:

  1. 首先,我们从react-hookedup中导入useArray Hook:
import React from 'react'
import { useArray } from 'react-hookedup'
  1. 然后,我们定义组件和 Array Hook,并将默认值设置为['one', 'two', 'three']
export default function UseArray () {
    const { value, add, clear, removeIndex } = useArray(['one', 'two', 'three'])
  1. 现在,我们将当前数组显示为 JSON:
    return (
        <div>
            <p>current array: {JSON.stringify(value)}</p>
  1. 然后,我们显示一个add按钮来添加一个元素:
            <button onClick={() => add('test')}>add element</button>
  1. 接下来,我们显示一个通过索引删除第一个元素的按钮:
            <button onClick={() => removeIndex(0)}>remove first element</button>
  1. 最后,我们添加一个clear按钮来清除所有元素:
            <button onClick={() => clear()}>clear elements</button>
        </div>
    )
}

正如我们所看到的,使用useArray Hook 使处理数组变得更简单。

useCounter Hook

useCounter Hook 可以用来定义各种类型的计数器。我们可以定义下限/上限,指定计数器是否应该循环,以及指定我们增加/减少计数器的步长。此外,Counter Hook 提供了函数来增加/减少计数器。

它接受以下配置选项:

  • upperLimit:定义计数器的上限(最大值)

  • lowerLimit:定义计数器的下限(最小值)

  • loop:指定计数器是否应该循环(例如,当达到最大值时,我们回到最小值)

  • step:设置增加和减少函数的默认步长

它返回以下对象:

  • value:我们计数器的当前值。

  • setValue:设置计数器的当前值。

  • increase:按给定的步长增加值。如果未指定数量,则使用默认步长。

  • decrease:按给定的步长减少值。如果未指定数量,则使用默认步长。

Counter Hook 可以如下使用:

  1. 首先,我们从react-hookedup中导入useCounter Hook:
import React from 'react'
import { useCounter } from 'react-hookedup'
  1. 然后,我们定义我们的组件和 Hook,指定0作为默认值。我们还指定upperLimitlowerLimitloop
export default function UseCounter () {
    const { value, increase, decrease } = useCounter(0, { upperLimit: 3, lowerLimit: 0, loop: true })
  1. 最后,我们渲染当前值和两个按钮来increase/decrease值:
    return (
        <div>
            <b>{value}</b>
            <button onClick={increase}>+</button>
            <button onClick={decrease}>-</button>
        </div>
    )
}

正如我们所看到的,Counter Hook 使得实现计数器变得更加简单。

焦点和悬停 Hooks

有时,我们想要检查用户是否悬停在元素上或者聚焦在input字段上。为了做到这一点,我们可以使用react-hookedup库提供的 Focus 和 Hover Hooks。

该库为这些特性提供了两个 Hooks:

  • useFocus Hook:处理焦点事件(例如,选择的input字段)

  • useHover Hook:处理悬停事件(例如,当鼠标指针悬停在一个区域上时)

useFocus Hook

为了知道一个元素当前是否聚焦,我们可以使用useFocus Hook 如下:

  1. 首先,我们导入useFocus Hook:
import React from 'react'
import { useFocus } from 'react-hookedup'
  1. 然后,我们定义我们的组件和 Focus Hook,它返回focused值和一个bind函数,将 Hook 绑定到一个元素:
export default function UseFocus () {
    const { focused, bind } = useFocus()
  1. 最后,我们渲染一个input字段,并将 Focus Hook 绑定到它:
    return (
        <div>
            <input {...bind} value={focused ? 'focused' : 'not focused'} />
        </div>
    )
}

正如我们所看到的,Focus Hook 使得处理焦点事件变得更加容易。不再需要定义我们自己的处理函数了。

useHover Hook

为了知道用户当前是否悬停在元素上,我们可以使用useHover Hook,如下所示:

  1. 首先,我们导入useHover Hook:
import React from 'react'
import { useHover } from 'react-hookedup'
  1. 然后,我们定义我们的组件和 Hover Hook,它返回hovered值和一个bind函数,将 Hook 绑定到元素:
export default function UseHover () {
    const { hovered, bind } = useHover()
  1. 最后,我们渲染一个元素,并将 Hover Hook 绑定到它:
    return (
        <div {...bind}>Hover me {hovered && 'THANKS!!!'}</div>
    )
}

正如我们所看到的,Hover Hook 使处理悬停事件变得更加容易。不再需要定义自己的处理程序函数。

示例代码

示例代码可以在Chapter08/chapter8_3文件夹中找到。

只需运行npm install来安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果不会自动打开)。

使用 Hooks 实现响应式设计

在 Web 应用程序中,拥有响应式设计通常很重要。响应式设计使您的 Web 应用程序在各种设备和窗口/屏幕尺寸上呈现良好。我们的博客应用可能在桌面上、手机上、平板上,甚至可能在非常大的屏幕上(如电视)上查看。

通常,对于响应式设计,最合理的方法是简单地使用 CSS 媒体查询。然而,有时这是不可能的,例如,当我们在画布或 Web 图形库(WebGL)中渲染元素时。有时,我们还希望根据窗口大小决定是否加载组件,而不是简单地渲染它,然后通过 CSS 隐藏它。

@rehooks/window-size库提供了useWindowSize Hook,返回以下值:

  • innerWidth:等同于window.innerWidth的值

  • innerHeight:等同于window.innerHeight的值

  • outerWidth:等同于window.outerWidth的值

  • outerHeight:等同于window.outerHeight的值

为了显示outerWidth/outerHeightinnerWidth/innerHeight之间的区别,请查看以下图表:

窗口宽度/高度属性的可视化

正如我们所看到的,innerHeightinnerWidth指定了浏览器窗口的最内部部分,而outerHeightouterWidth指定了浏览器窗口的完整尺寸,包括 URL 栏、滚动条等。

现在,我们将根据博客应用中的窗口大小隐藏组件。

响应式隐藏组件

在我们的博客应用中,当屏幕尺寸非常小时,我们将完全隐藏UserBarChangeTheme组件,这样在手机上阅读文章时,我们可以专注于内容。

让我们开始实现 Window Size Hook:

  1. 首先,我们必须安装@rehooks/window-size库:
> npm install --save @rehooks/window-size
  1. 然后,在src/pages/HeaderBar.js文件的开头导入useWindowSize Hook:
import useWindowSize from '@rehooks/window-size'
  1. 接下来,在现有的 Context Hooks 之后,我们定义以下 Window Size Hook:
            const { innerWidth } = useWindowSize()
  1. 如果窗口宽度小于640像素,我们假设设备是手机:
            const mobilePhone = innerWidth < 640
  1. 最后,只有在不是手机上时,我们才显示 ChangeTheme 和 UserBar 组件:
 {!mobilePhone && <ChangeTheme theme={theme} setTheme={setTheme} />}
             {!mobilePhone && <br />}
             {!mobilePhone && <React.Suspense fallback={"Loading..."}>
                 <UserBar />
             </React.Suspense>}
             {!mobilePhone && <br />} 

如果我们现在调整浏览器窗口的宽度小于640像素,我们可以看到ChangeThemeUserBar组件将不再被渲染:

在较小的屏幕尺寸上隐藏 ChangeTheme 和 UserBar 组件

使用 Window Size Hook,我们可以避免在较小的屏幕尺寸上渲染元素。

示例代码

示例代码可以在Chapter08/chapter8_4文件夹中找到。

只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

使用 Hooks 进行撤消/重做

在一些应用中,我们希望实现撤消/重做功能,这意味着我们可以在应用的状态中前进和后退。例如,如果我们在博客应用中有一个文本编辑器,我们希望提供撤消/重做更改的功能。如果你了解 Redux,你可能已经熟悉这种功能。由于 React 现在提供了 Reducer Hook,我们可以只使用 React 重新实现相同的功能。use-undo库正好提供了这种功能。

useUndo Hook 接受默认的state对象作为参数,并返回一个包含以下内容的数组:[ state, functions ]

state对象如下所示:

  • present:当前状态

  • past:过去状态的数组(当我们撤消时,我们会回到这里)

  • future:未来状态的数组(撤消后,我们可以重做到这里)

functions对象返回与 Undo Hook 交互的各种函数:

  • set:设置当前状态,并为present分配一个新值。

  • reset:重置当前状态,清除pastfuture数组(撤消/重做历史记录),并为present分配一个新值。

  • undo:撤销到先前的状态(遍历past数组的元素)。

  • redo:重做到下一个状态(遍历future数组的元素)。

  • canUndo:如果可以执行撤销操作(past数组不为空),则为true

  • canRedo:如果可以执行重做操作(future数组不为空),则为true

我们现在将在我们的文章编辑器中实现撤销/重做功能。

在我们的文章编辑器中实现撤销/重做

在我们博客应用的简单文章编辑器中,我们有一个textarea,我们可以在其中编写博客文章的内容。现在我们将在那里实现useUndo Hook,这样我们就可以撤销/重做对文本所做的任何更改:

  1. 首先,我们必须通过npm安装use-undo库:
> npm install --save use-undo
  1. 然后,我们在src/post/CreatePost.js中从库中导入useUndo Hook:
import useUndo from 'use-undo'
  1. 接下来,通过替换当前的useInput Hook 来定义 Undo Hook。删除以下代码行:
    const { value: content, bindToInput: bindContent } = useInput('')

useUndo Hook 替换它,如下所示。我们将默认状态设置为''。我们还将状态保存到undoContent,并获取setContentundoredo函数,以及canUndocanRedo值:

    const [ undoContent, {
        set: setContent,
        undo,
        redo,
        canUndo,
        canRedo
    } ] = useUndo('')
  1. 现在,我们将undoContent.present状态分配给content变量:
    const content = undoContent.present
  1. 接下来,我们定义一个新的处理函数,以便使用setContent函数更新content值:
    function handleContent (e) {
        setContent(e.target.value)
    }
  1. 然后,我们必须用handleContent函数替换bindContent对象,如下所示:
            <textarea value={content} onChange={handleContent} />
  1. 最后,在textarea元素之后定义按钮来撤销/重做我们的更改:
            <button type="button" onClick={undo} disabled={!canUndo}>Undo</button>
            <button type="button" onClick={redo} disabled={!canRedo}>Redo</button>

<form>元素中,<button>元素具有定义的type属性是很重要的。如果未定义type属性,则假定按钮的type"submit",这意味着当点击时它们将触发onSubmit处理函数。

现在,在输入文本后,我们可以按 Undo 逐个删除一个字符,然后按 Redo 再次添加字符。接下来,我们将实现去抖动,这意味着我们的更改只会在一定时间后添加到撤销历史记录中,而不是在每输入一个字符后。

使用 Hooks 进行去抖动

正如我们在前一节中所看到的,当我们按下 Undo 时,它会逐个撤销一个字符。有时,我们不希望将每个更改都存储在我们的撤销历史记录中。为了避免存储每个更改,我们需要实现去抖动,这意味着将我们的content存储到撤销历史记录的函数只在一定时间后才会被调用。

use-debounce库提供了useDebounce Hook,可以用于简单值,如下所示:

const [ text, setText ] = useState('')
const [ value ] = useDebounce(text, 1000)

现在,如果我们通过setText更改文本,text值将立即更新,但value变量将在1000毫秒(1秒)后更新。

然而,对于我们的用例来说,这还不够。我们需要去抖动回调来结合use-undo实现去抖动。use-debounce库还提供了useDebouncedCallback Hook,可以如下使用:

const [ text, setText ] = useState('')
const [ debouncedSet, cancelDebounce ] = useDebouncedCallback(
    (value) => setText(value),
    1000
)

现在,如果我们调用debouncedSet('text')text值将在1000毫秒(1秒)后更新。如果多次调用debouncedSet,超时时间将每次重置,因此只有在1000毫秒内没有进一步调用debouncedSet函数时,才会调用setText函数。接下来,我们将继续实现帖子编辑器中的去抖动。

在我们的帖子编辑器中去抖动变化

现在我们已经了解了去抖动,我们将在帖子编辑器中与撤销 Hook 结合实现它,如下所示:

  1. 首先,我们必须通过npm安装use-debounce库:
> npm install --save use-debounce
  1. src/post/CreatePost.js中,首先确保导入useState Hook,如果尚未导入:
import React, { useState, useContext, useEffect } from 'react'
  1. 接下来,从use-debounce库中导入useDebouncedCallback Hook:
import { useDebouncedCallback } from 'use-debounce'
  1. 现在,在撤销 Hook 之前,定义一个新的 State Hook,我们将用它来更新input字段的非去抖动值:
    const [ content, setInput ] = useState('')
  1. 在撤销 Hook 之后,我们移除content值的赋值。移除以下代码:
    const content = undoContent.present
  1. 现在,在撤销 Hook 之后,定义去抖动回调 Hook:
    const [ setDebounce, cancelDebounce ] = useDebouncedCallback(
  1. 在去抖动回调 Hook 中,我们定义一个函数来设置撤销 Hook 的内容:
        (value) => {
            setContent(value)
        },
  1. 我们在200毫秒后触发setContent函数:
        200
    )
  1. 接下来,我们必须定义一个 Effect Hook,每当撤销状态改变时触发。在这个 Effect Hook 中,我们取消当前的去抖动,并将content值设置为当前的present值:
    useEffect(() => {
        cancelDebounce()
        setInput(undoContent.present)
    }, [undoContent])
  1. 最后,我们调整handleContent函数以触发setInput函数和setDebounce函数:
    function handleContent (e) 
        const { value } = e.target
        setInput(value)
        setDebounce(value)
    }

因此,我们立即设置输入的value,但我们还没有将任何内容存储到撤销历史中。在去抖回调触发后(200毫秒后),我们将当前值存储到撤销历史中。每当撤销状态更新时,例如当我们按下撤销/重做按钮时,我们取消当前的去抖以避免在撤销/重做后覆盖值。然后,我们将content值设置为撤销 Hook 的新present值。

如果我们现在在编辑器中输入一些文本,我们会看到撤销按钮只有在一段时间后才会激活。然后它看起来像这样:

在输入一些文本后激活撤销按钮

如果我们现在按下撤销按钮,我们会看到我们不是逐个字符地撤销,而是一次撤销更多的文本。例如,如果我们按三次撤销,我们会得到以下结果:

使用撤销按钮回到过去

我们可以看到,撤销/重做和去抖现在都运行得很好!

示例代码

示例代码可以在Chapter08/chapter8_5文件夹中找到。

只需运行npm install来安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果不会自动打开)。

查找其他 Hooks

还有许多其他由社区提供的 Hooks。您可以在以下页面上找到各种 Hooks 的可搜索列表:nikgraf.github.io/react-hooks/.

为了让你了解其他还有哪些 Hooks,以下功能由社区提供的 Hooks 提供。我们现在列出了社区提供的一些更有趣的 Hooks。当然,还有很多其他的 Hooks 可以找到:

总结

在本章中,我们首先了解了react-hookedup库。我们在博客应用中使用这个库来简化使用 Hooks 处理输入。然后,我们看了一下如何使用 Hooks 实现各种 React 生命周期。接下来,我们介绍了各种有用的 Hooks,比如usePrevious Hook,Interval/Timeout Hooks,Online Status Hook,数据操作 Hooks,以及 Focus 和 Hover Hooks。之后,我们使用 Hooks 实现了响应式设计,不在手机上渲染某些组件。最后,我们学习了如何使用 Hooks 实现撤销/重做功能和防抖。

使用社区 Hooks 是一项非常重要的技能,因为 React 只提供了一小部分 Hooks。在实际应用中,你可能会使用许多由社区提供的 Hooks,来自各种库和框架。我们还学习了一些社区提供的 Hooks,这些 Hooks 在编写 React 应用时会让我们的生活变得更加轻松。

在下一章中,我们将深入了解 Hooks 的规则,这些规则在我们开始编写自己的 Hooks 之前是很重要的。

问题

为了回顾本章学到的内容,请尝试回答以下问题:

  1. 我们可以使用哪个 Hook 来简化输入字段处理?

  2. 如何使用 Effect Hooks 来实现componentDidMountcomponentWillUnmount生命周期?

  3. 我们如何使用 Hooks 来实现this.setState()的行为?

  4. 为什么我们应该使用定时器 Hooks 而不是直接调用setTimeoutsetInterval

  5. 我们可以使用哪些 Hooks 来简化处理常见数据结构?

  6. 何时应该使用 Hooks 来实现响应式设计,而不是简单地使用 CSS 媒体查询?

  7. 我们可以使用哪个 Hook 来实现撤销/重做功能?

  8. 什么是防抖?为什么我们需要这样做?

  9. 我们可以使用哪些 Hooks 来实现防抖?

进一步阅读

如果您对本章学习的概念更多信息感兴趣,请查看以下阅读材料:

第九章:Hooks 的规则

在上一章中,我们学习了如何使用由 React 社区开发的各种 Hooks,以及在哪里找到更多的 Hooks。我们学习了如何用 Hooks 替换 React 生命周期方法,使用实用程序和数据管理 Hooks,使用 Hooks 进行响应式设计,以及使用 Hooks 实现撤销/重做功能。最后,我们学习了在哪里找到其他 Hooks。

在本章中,我们将学习有关使用 Hooks 的一切知识,以及在使用和开发自己的 Hooks 时需要注意的事项。Hooks 在调用顺序方面有一定的限制。违反 Hooks 的规则可能会导致错误或意外行为,因此我们需要确保学习并强制执行规则。

本章将涵盖以下主题:

  • 调用 Hooks

  • Hooks 的顺序

  • Hooks 的名称

  • 强制执行 Hooks 的规则

  • 处理useEffect的依赖关系

技术要求

应该已经安装了相当新的 Node.js 版本(v11.12.0 或更高)。还需要安装 Node.js 的npm包管理器。

本章的代码可以在 GitHub 存储库中找到:github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter09

查看以下视频以查看代码的运行情况:

bit.ly/2Mm9yoC

请注意,强烈建议您自己编写代码。不要简单地运行提供的代码示例。重要的是要自己编写代码以便正确学习和理解。但是,如果遇到任何问题,您可以随时参考代码示例。

现在,让我们开始本章。

调用 Hooks

Hooks 应该只在React 函数组件自定义 Hooks中调用。它们不能在类组件或常规 JavaScript 函数中使用。

Hooks 可以在以下顶层调用:

  • React 函数组件

  • 自定义 Hooks(我们将在下一章学习如何创建自定义 Hooks)

正如我们所看到的,Hooks 大多是普通的 JavaScript 函数,只是它们依赖于在 React 函数组件中定义。当然,使用其他 Hooks 的自定义 Hooks 可以在 React 函数组件之外定义,但是在使用Hooks 时,我们总是需要确保在 React 函数组件内调用它们。接下来,我们将学习有关 Hooks 顺序的规则。

Hooks 的顺序

只在函数组件或自定义 Hooks 的顶层/开头调用 Hooks。

不要在条件、循环或嵌套函数中调用 Hooks——这样会改变 Hooks 的顺序,导致错误。我们已经学到改变 Hooks 的顺序会导致多个 Hooks 之间的状态混乱。

在第二章中,使用 State Hook,我们学到不能做以下事情:

const [ enableFirstName, setEnableFirstName ] = useState(false)
const [ name, setName ] = enableFirstName
 ? useState('')
 : [ '', () => {} ] const [ lastName, setLastName ] = useState('')

我们渲染了一个复选框和两个输入字段用于firstNamelastName,然后在lastName字段中输入了一些文本:

重新查看我们在第二章“使用 State Hook”中的示例

目前,Hooks 的顺序如下:

  1. enableFirstName

  2. lastName

接下来,我们点击复选框以启用firstName字段。这样做改变了 Hooks 的顺序,因为现在我们的 Hook 定义如下:

  1. enableFirstName

  2. firstName

  3. lastName

由于 React 仅依赖于 Hooks 的顺序来管理它们的状态,firstName字段现在是第二个 Hook,因此它从lastName字段获取状态:

从第二章“使用 State Hook”中改变 Hooks 的问题

如果我们在第二章的示例 2“我们能定义条件 Hooks 吗?”中使用 React 中真正的useState Hook,我们可以看到 React 会自动检测 Hooks 的顺序是否改变,并显示警告:

React 在检测到 Hooks 的顺序已改变时打印警告

在开发模式下运行 React 时,如果渲染的 Hooks 数量比上一次渲染多,它还会崩溃并显示一个 Uncaught Invariant Violation 错误消息:

在开发模式下,当 Hooks 的数量改变时,React 会崩溃

正如我们所看到的,改变 Hooks 的顺序或有条件地启用 Hooks 是不可能的,因为 React 在内部使用 Hooks 的顺序来跟踪哪些数据属于哪个 Hook。

Hooks 的命名

有一个约定,即 Hook 函数应始终以use为前缀,后面跟着以大写字母开头的 Hook 名称;例如:useStateuseEffectuseResource。这很重要,因为否则我们将不知道哪些 JavaScript 函数是 Hooks,哪些不是。特别是在强制执行 Hooks 的规则时,我们需要知道哪些函数是 Hooks,以便确保它们不会在条件语句或循环中被调用。

正如我们所看到的,命名约定在技术上并不是必需的,但对开发人员来说会大大简化生活。知道普通函数和 Hooks 之间的区别使得自动执行 Hooks 的规则变得非常容易。在下一节中,我们将学习如何使用eslint工具自动执行规则。

强制执行 Hooks 的规则

如果我们遵循在 Hook 函数前加上use的约定,我们可以自动执行另外两条规则:

  • 只从 React 函数组件或自定义 Hooks 中调用 Hooks

  • 只在顶层调用 Hooks(不在循环、条件或嵌套函数内部)

为了自动执行规则,React 提供了一个名为eslint-plugin-react-hookseslint插件,它将自动检测何时使用了 Hooks,并确保规则不被违反。ESLint 是一个代码检查工具,它分析源代码并找出样式错误、潜在的 bug 和编程错误等问题。

将来,create-react-app将默认包含此插件。

设置 eslint-plugin-react-hooks

我们现在要设置 React Hooks eslint插件,自动执行 Hooks 的规则。

让我们开始安装和启用eslint插件:

  1. 首先,我们必须通过npm安装插件:
> npm install --save-dev eslint-plugin-react-hooks

我们在这里使用--save-dev标志,因为在部署应用程序时不需要安装eslint及其插件。我们只在开发应用程序时需要它们。

  1. 然后,在项目文件夹的根目录下创建一个新的.eslintrc.json文件,内容如下。我们首先从react-app的 ESLint 配置中扩展:
{
    "extends": "react-app",
  1. 接下来,我们包括之前安装的react-hooks插件。
    "plugins": [
        "react-hooks"
    ],
  1. 现在我们启用了两个规则。首先,我们告诉 eslint 在违反 rules-of-hooks 规则时显示错误。此外,我们将 exhaustive-deps 规则设置为警告:
    "rules": {
        "react-hooks/rules-of-hooks":  "error",
        "react-hooks/exhaustive-deps":  "warn"
    }
}
  1. 最后,我们调整 package.json 来定义一个新的 lint 脚本,它将调用 eslint
    "scripts": {
 "lint": "npx eslint src/",

现在,我们可以执行 npm run lint,然后我们会看到有 5 个警告和 0 个错误:

使用 react-hooks 插件执行 ESLint

现在我们将尝试违反 Hooks 规则;例如,通过编辑 src/user/Login.js 并使第二个 Input Hook 有条件:

 const  {  value:  password,  bindToInput:  bindPassword  }  =  loginFailed  ? useInput('') : [ '',  ()  =>  {} ]

再次执行 npm run lint 时,我们可以看到现在有一个错误:

在违反 Hooks 规则后执行 ESLint

正如我们所看到的,eslint 通过强制我们遵守 Hooks 规则来帮助我们。当我们违反任何规则时,linter 会抛出错误,并在 Effect Hooks 缺少依赖项时显示警告。听从 eslint 将帮助我们避免错误和意外行为,因此我们永远不应该忽略它的错误或警告。

示例代码

示例代码可以在 Chapter09/chapter9_1 文件夹中找到。

只需运行 npm install 来安装所有依赖项,并执行 npm run lint 来运行 linter。

处理 useEffect 依赖关系

除了强制执行 Hooks 规则之外,我们还检查了在 Effect Hook 中使用的所有变量是否都传递给了它的依赖数组。这个 详尽的依赖 规则确保每当 Effect Hook 中使用的东西发生变化(函数、值等),Hook 就会再次触发。

正如我们在前一节中看到的,当使用 npm run lint 运行 linter 时,会有一些与详尽的依赖规则相关的警告。通常,这与 dispatch 函数或其他函数不在依赖数组中有关。通常情况下,这些函数不应该改变,但我们永远不能确定,所以最好将它们添加到依赖项中。

使用 eslint 自动修复警告

由于详尽的依赖规则相当简单且容易修复,我们可以让 eslint 自动修复它。

为此,我们需要向 eslint 传递 --fix 标志。使用 npm run,我们可以通过使用额外的 -- 作为分隔符来传递标志,如下所示:

> npm run lint -- --fix

在运行了上述命令之后,我们可以再次运行 npm run lint,然后我们会看到所有警告都已经自动修复了:

让 eslint 修复后没有警告

正如我们所看到的,eslint不仅警告我们有问题,甚至可以自动为我们修复其中一些问题!

示例代码

示例代码可以在Chapter09/chapter9_2文件夹中找到。

只需运行npm install来安装所有依赖项,并执行npm run lint来运行 linter。

摘要

在本章中,我们首先了解了 Hooks 的两个规则:我们应该只从 React 函数组件中调用 Hooks,并且我们需要确保 Hooks 的顺序保持不变。此外,我们还了解了 Hooks 的命名约定,它们应该始终以use前缀开头。然后,我们学习了如何使用eslint强制执行 Hooks 的规则。最后,我们学习了关于useEffect依赖项,以及如何使用eslint自动修复缺少的依赖项。

了解 Hooks 的规则,并强制执行它们,对于避免错误和意外行为非常重要。在创建我们自己的 Hooks 时,这些规则将特别重要。现在我们对 Hooks 的工作原理有了很好的理解,包括它们的规则和约定,在下一章中,我们将学习如何创建我们自己的 Hooks!

问题

为了总结本章学到的知识,请尝试回答以下问题:

  1. Hooks 可以在哪里调用?

  2. 我们可以在 React 类组件中使用 Hooks 吗?

  3. 关于 Hooks 的顺序,我们需要注意什么?

  4. Hooks 可以在条件、循环或嵌套函数中调用吗?

  5. Hooks 的命名约定是什么?

  6. 我们如何自动强制执行 Hooks 的规则?

  7. 什么是完整的依赖规则?

  8. 我们如何自动修复 linter 警告?

进一步阅读

如果您对本章学到的概念感兴趣,可以查看以下阅读材料:

第十章:构建自己的 Hooks

在上一章中,我们了解了 Hooks 的限制和规则。我们了解了在哪里调用 Hooks,为什么 Hooks 的顺序很重要,以及 Hooks 的命名约定。最后,我们学习了如何强制执行 Hooks 的规则以及处理useEffect的依赖关系。

在本章中,我们将学习如何通过从组件中提取现有代码来创建自定义 Hooks。我们还将学习如何使用自定义 Hooks 以及 Hooks 如何相互交互。然后,我们将学习如何为我们的自定义 Hooks 编写测试。最后,我们将学习有关完整的 React Hooks API。

本章将涵盖以下主题:

  • 提取自定义 Hooks

  • 使用自定义 Hooks

  • Hooks 之间的交互

  • 测试 Hooks

  • 探索 React Hooks API

技术要求

应该已经安装了相当新的 Node.js 版本(v11.12.0 或更高)。还需要安装 Node.js 的npm包管理器。

本章的代码可以在 GitHub 上找到:github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter10

查看以下视频,以查看代码的实际运行情况:

bit.ly/2Mm9yoC

请注意,强烈建议您自己编写代码。不要简单地运行之前提供的代码示例。重要的是要自己编写代码,以便正确学习和理解。但是,如果遇到任何问题,您可以随时参考代码示例。

现在让我们开始本章。

提取自定义 Hooks

通过学习 State 和 Effect Hooks、community Hooks 以及 Hooks 的规则,我们对 Hooks 的概念有了很好的理解,现在我们将构建自己的 Hooks。我们首先从我们的博客应用程序的现有功能中提取自定义 Hooks。通常情况下,如果我们注意到我们在多个组件中使用类似的代码,那么首先编写组件,然后稍后从中提取自定义 Hook 是最合理的。这样做可以避免过早地定义自定义 Hooks,并使我们的项目变得不必要地复杂。

在本节中,我们将提取以下 Hooks:

  • useTheme Hook

  • useUserStateusePostsState Hooks

  • useDispatch Hook

  • API Hooks

  • useDebouncedUndo Hook

创建一个 useTheme Hook

在许多组件中,我们使用 ThemeContext 来为我们的博客应用程序设置样式。通常在多个组件中使用的功能通常是创建自定义 Hook 的好机会。正如你可能已经注意到的,我们经常做以下事情:

import { ThemeContext } from '../contexts'

export default function SomeComponent () {
    const theme = useContext(ThemeContext)

    // ...

我们可以将这个功能抽象成一个 useTheme Hook,它将从 ThemeContext 中获取 theme 对象。

让我们开始创建一个自定义的 useTheme Hook:

  1. 创建一个新的 src/hooks/ 目录,这是我们将放置自定义 Hooks 的地方。

  2. 创建一个新的 src/hooks/useTheme.js 文件。

  3. 在这个新创建的文件中,我们首先导入 useContext Hook 和 ThemeContext 如下:

import { useContext } from 'react'
import { ThemeContext } from '../contexts'
  1. 接下来,我们导出一个名为 useTheme 的新函数;这将是我们的自定义 Hook。记住,Hooks 只是以 use 关键字为前缀的函数:
export default function useTheme () {
  1. 在我们的自定义 Hook 中,我们现在可以使用 React 提供的基本 Hooks 来构建我们自己的 Hook。在我们的情况下,我们只是返回 useContext Hook:
    return useContext(ThemeContext)
}

我们可以看到,自定义 Hooks 可以非常简单。在这种情况下,自定义 Hook 只返回一个传递给它的 ThemeContext 的 Context Hook。然而,这使我们的代码更简洁,以后更容易更改。此外,通过使用 useTheme Hook,我们清楚地表明我们想要访问主题,这意味着我们的代码将更容易阅读和理解。

创建全局状态 Hooks

我们经常做的另一件事是访问全局状态。例如,一些组件需要 user 状态,一些需要 posts 状态。为了抽象这个功能,这也将使以后更容易调整状态结构,我们可以创建自定义 Hooks 来获取状态的特定部分:

  • useUserState:获取 state 对象的 user 部分

  • usePostsState:获取 state 对象的 posts 部分

定义 useUserState Hook

重复类似于我们为 useTheme Hook 所做的过程,我们从 React 中导入 useContext Hook 和 StateContext。然而,我们现在不返回 Context Hook 的结果,而是通过解构提取 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 Hook 类似,useUserState Hook 使我们的代码更简洁,以后更容易更改,并提高了可读性。

定义 usePostsState Hook

我们对 posts 状态重复相同的过程。创建一个新的 src/hooks/usePostsState.js 文件,内容如下:

import { useContext } from 'react'
import { StateContext } from '../contexts'

export default function usePostsState () {
    const { state } = useContext(StateContext)
    return state.posts
}

useThemeuseUserState Hooks 类似,usePostsState Hook 使我们的代码更简洁,更容易以后更改,并提高了可读性。

创建一个 useDispatch Hook

在许多组件中,我们需要dispatch函数来执行某些操作,所以我们经常需要做以下操作:

import { StateContext } from '../contexts'

export default function SomeComponent () {
    const { dispatch } = useContext(StateContext)

    // ...

我们可以将这个功能抽象成一个useDispatch Hook,它将从全局状态上下文中获取dispatch函数。这样做也会使以后更容易替换状态管理实现。例如,以后我们可以用 Redux 或 MobX 等状态管理库来替换我们简单的 Reducer Hook。

让我们现在按照以下步骤定义useDispatch Hook:

  1. 创建一个新的src/hooks/useDispatch.js文件。

  2. 从 React 中导入useContext Hook 和StateContext如下:

import { useContext } from 'react'
import { StateContext } from '../contexts'
  1. 接下来,我们定义并导出useDispatch函数;在这里,我们允许传递不同的context作为参数,以使 Hook 更通用(以防以后我们想要从本地状态上下文中使用dispatch函数)。然而,我们将context参数的默认值设置为StateContext,如下所示:
export default function useDispatch (context = StateContext) {
  1. 最后,我们通过解构从 Context Hook 中提取dispatch函数,并使用以下代码返回它:
    const { dispatch } = useContext(context)
    return dispatch
}

正如我们所看到的,创建自定义 Dispatch Hook 使我们的代码更容易以后更改,因为我们只需要在一个地方调整dispatch函数。

创建 API Hooks

我们还可以为各种 API 调用创建 Hooks。将这些 Hooks 放在一个单独的文件中可以让我们以后更容易调整 API 调用。我们将用useAPI前缀来命名我们的自定义 API Hooks,这样很容易区分哪些函数是 API Hooks。

让我们现在按照以下步骤为我们的 API 创建自定义 Hooks:

  1. 创建一个新的src/hooks/api.js文件。

  2. react-request-hook库中导入useResource Hook 如下:

import { useResource } from 'react-request-hook'
  1. 首先,我们定义一个useAPILogin Hook 来登录用户;我们只需从src/user/Login.js文件中剪切并粘贴现有的代码如下:
export function useAPILogin () {
    return useResource((username, password) => ({
        url: `/login/${encodeURI(username)}/${encodeURI(password)}`,
        method: 'get'
    }))
}
  1. 接下来,我们定义一个useAPIRegister Hook;我们只需从src/user/Register.js文件中剪切并粘贴现有的代码如下:
export function useAPIRegister () {
    return useResource((username, password) => ({
        url: '/users',
        method: 'post',
        data: { username, password }
    }))
}
  1. 现在我们定义一个useAPICreatePost Hook,从src/post/CreatePost.js文件中剪切并粘贴现有的代码如下:
export function useAPICreatePost () {
    return useResource(({ title, content, author }) => ({
        url: '/posts',
        method: 'post',
        data: { title, content, author }
    }))
}
  1. 最后,我们定义一个useAPIThemes Hook,从src/ChangeTheme.js文件中剪切并粘贴现有的代码如下:
export function useAPIThemes () {
    return useResource(() => ({
        url: '/themes',
        method: 'get'
    }))
}

正如我们所看到的,将所有与 API 相关的功能放在一个地方,可以更容易地在以后调整我们的 API 代码。

创建一个 useDebouncedUndo Hook

现在我们将创建一个稍微更高级的用于防抖撤销功能的 Hook。我们已经在CreatePost组件中实现了这个功能。现在,我们将把这个功能提取到一个自定义的useDebouncedUndo Hook 中。

让我们按照以下步骤创建useDebouncedUndo Hook:

  1. 创建一个新的src/hooks/useDebouncedUndo.js文件。

  2. 从 React 中导入useStateuseEffectuseCallback Hooks,以及useUndo Hook 和useDebouncedCallback Hook:

import { useState, useEffect, useCallback } from 'react'
import useUndo from 'use-undo'
import { useDebouncedCallback } from 'use-debounce'
  1. 现在我们将定义useDebouncedUndo函数,它接受一个用于防抖回调的timeout参数:
export default function useDebouncedUndo (timeout = 200) {
  1. 在这个函数中,我们从先前的实现中复制了useState Hook,如下所示:
    const [ content, setInput ] = useState('')
  1. 接下来,我们复制useUndo Hook;但是,这一次,我们将所有其他与撤销相关的函数存储在一个undoRest对象中:
    const [ undoContent, { set: setContent, ...undoRest } ] = useUndo('')
  1. 然后,我们复制useDebouncedCallback Hook,用我们的timeout参数替换固定的200值:
    const [ setDebounce, cancelDebounce ] = useDebouncedCallback(
        (value) => {
            setContent(value)
        },
        timeout
    )
  1. 现在我们复制 Effect Hook,如下所示的代码:
    useEffect(() => {
        cancelDebounce()
        setInput(undoContent.present)
    }, [cancelDebounce, undoContent])
  1. 然后,我们定义一个setter函数,它将设置一个新的输入value并调用setDebounce。我们可以在这里使用useCallback Hook 来包装setter函数,以返回函数的记忆版本,并避免在使用 Hook 的组件重新渲染时每次重新创建函数。与useEffectuseMemo Hook 类似,我们还将一个依赖数组作为useCallback Hook 的第二个参数传递:
    const setter = useCallback(function setterFn (value) {
        setInput(value)
        setDebounce(value)
    }, [ setInput, setDebounce ])
  1. 最后,我们返回content变量(包含当前输入value)、setter函数和undoRest对象(其中包含undo/redo函数和canUndo/canRedo布尔值):
    return [ content, setter, undoRest ]
}

创建一个用于防抖撤销的自定义 Hook 意味着我们可以在多个组件中重用该功能。我们甚至可以将此 Hook 提供为公共库,让其他人轻松实现防抖撤销功能。

导出我们的自定义 Hooks

在创建了所有我们的自定义 Hooks 之后,我们将在我们的 Hooks 目录中创建一个index.js文件,并在那里重新导出我们的 Hooks,这样我们就可以按照以下方式导入我们的自定义 Hooks:import { useTheme } from './hooks'

现在让我们按照以下步骤导出所有我们的自定义 Hooks:

  1. 创建一个新的src/hooks/index.js文件。

  2. 在这个文件中,我们首先导入我们的自定义 Hooks 如下:

import useTheme from './useTheme'
import useDispatch from './useDispatch'
import usePostsState from './usePostsState'
import useUserState from './useUserState'
import useDebouncedUndo from './useDebouncedUndo'
  1. 然后,我们使用以下代码重新导出这些导入的 Hooks:
export { useTheme, useDispatch, usePostsState, useUserState, useDebouncedUndo }
  1. 最后,我们从api.js文件中重新导出所有的 Hooks,如下所示:
export * from './api'

现在我们已经导出了所有自定义的 Hooks,我们可以直接从hooks文件夹中导入 Hooks,这样可以更容易地一次导入多个自定义的 Hooks。

示例代码

示例代码可以在Chapter10/chapter10_1文件夹中找到。

只需运行npm install来安装所有的依赖项,然后运行npm start来启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

使用我们的自定义 Hooks

创建了我们的自定义 Hooks 之后,我们现在可以开始在整个博客应用程序中使用它们。使用自定义 Hooks 非常简单,因为它们类似于社区 Hooks。就像所有其他 Hooks 一样,自定义 Hooks 只是 JavaScript 函数。

我们创建了以下的 Hooks:

  • useTheme

  • useDispatch

  • usePostsState

  • useUserState

  • useDebouncedUndo

  • useAPILogin

  • useAPIRegister

  • useAPICreatePost

  • useAPIThemes

在这一部分,我们将重构我们的应用程序来使用所有自定义的 Hooks。

使用 useTheme Hook

现在,我们可以直接使用useTheme Hook,而不是使用ThemeContextuseContext Hook!如果以后我们改变了主题系统,我们只需修改useTheme Hook,新系统就会在整个应用程序中实现。

让我们重构我们的应用程序来使用useTheme Hook:

  1. 编辑src/Header.js,并用useTheme Hook 的导入替换现有的导入。ThemeContextuseContext的导入可以被移除:
import { useTheme } from './hooks'
  1. 然后,将当前的 Context Hook 定义替换为useTheme Hook,如下所示:
    const { primaryColor } = useTheme()
  1. 现在编辑src/post/Post.js,并在那里进行类似的导入调整:
import { useTheme } from './hooks'
  1. 然后,将useContext Hook 替换为以下的useTheme Hook:
    const { secondaryColor } = useTheme()

正如我们所看到的,使用自定义 Hook 使我们的代码更加简洁和易于阅读。现在我们继续使用全局状态的 Hooks。

使用全局状态的 Hooks

与我们对ThemeContext所做的类似,我们也可以用usePostsStateuseUserStateuseDispatch Hook 替换我们的状态 Context Hooks。如果以后我们想要更改状态逻辑,这是最佳的选择。例如,如果我们的状态增长并且我们想要使用更复杂的系统,比如 Redux 或 MobX,那么我们只需调整现有的 Hooks,一切都会像以前一样工作。

在这一部分,我们将调整以下组件:

  • UserBar

  • Login

  • Register

  • Logout

  • CreatePost

  • PostList

调整UserBar组件

首先,我们将调整UserBar组件。在这里,我们可以按照以下步骤使用useUserState Hook:

  1. 编辑src/user/UserBar.js并导入useUserState Hook:
import { useUserState } from '../hooks'
  1. 然后,我们移除以下 Hook 定义:
    const { state } = useContext(StateContext)
    const { user } = state
  1. 我们用我们自定义的useUserState Hook 替换它:
    const user = useUserState()

现在UserBar组件使用我们的自定义 Hook,而不是直接访问user状态。

调整Login组件

接下来,我们将调整Login组件,这里我们可以使用useDispatch Hook。具体步骤如下所述:

  1. 编辑src/user/Login.js并导入useDispatch Hook,如下所示:
import { useDispatch } from '../hooks'
  1. 然后移除以下 Context Hook:
    const { dispatch } = useContext(StateContext)
  1. 用我们自定义的useDispatch Hook 替换它:
    const dispatch = useDispatch()

现在Login组件使用我们的自定义 Hook,而不是直接访问dispatch函数。接下来,我们将调整Register组件。

调整Register组件

Login组件类似,我们也可以在Register组件中使用useDispatch Hook,具体步骤如下所示:

  1. 编辑src/user/Register.js并导入useDispatch Hook:
import { useDispatch } from '../hooks'
  1. 然后,用我们自定义的 Dispatch Hook 替换当前的 Context Hook,如下所示:
    const dispatch = useDispatch()

现在Register组件也使用我们的自定义 Hook,而不是直接访问dispatch函数。

调整Logout组件

然后,我们将调整Logout组件,以使用useUserStateuseDispatch Hooks,具体步骤如下:

  1. 编辑src/user/Logout.js并导入useUserStateuseDispatch Hooks:
import { useDispatch, useUserState } from '../hooks'
  1. 然后,用以下内容替换当前的 Hook 定义:
    const dispatch = useDispatch()
    const user = useUserState()

现在Logout组件使用我们的自定义 Hooks,而不是直接访问user状态和dispatch函数。

调整CreatePost组件

接下来,我们将调整CreatePost组件,这与我们对Logout组件所做的类似。具体步骤如下所述:

  1. 编辑src/post/CreatePost.js并导入useUserStateuseDispatch Hooks:
import { useUserState, useDispatch } from '../hooks'
  1. 然后,用以下内容替换当前的 Context Hook 定义:
    const user = useUserState()
    const dispatch = useDispatch()

现在CreatePost组件使用我们的自定义 Hooks,而不是直接访问user状态和dispatch函数。

调整PostList组件

最后,我们将使用usePostsState Hook 来渲染PostList组件,如下所示:

  1. 编辑src/post/PostList.js并导入usePostsState Hook:
import { usePostsState } from '../hooks'
  1. 然后用以下内容替换当前的 Hook 定义:
    const posts = usePostsState()

现在PostList组件使用我们自定义的 Hook 而不是直接访问posts状态。

使用 API Hooks

接下来,我们将用我们自定义的 API Hooks 替换所有useResource Hooks。这样做可以让我们将所有 API 调用放在一个文件中,以便以后可以轻松调整它们,以防 API 发生变化。

在本节中,我们将调整以下组件:

  • ChangeTheme

  • Register

  • Login

  • CreatePost

让我们开始吧。

调整 ChangeTheme 组件

首先,我们将调整ChangeTheme组件,并用我们自定义的useAPIThemes Hook 替换访问/themes的 Resource Hook,步骤如下:

  1. src/ChangeTheme.js中,删除以下useResource Hook 导入语句:
import { useResource } from 'react-request-hook'

用我们自定义的useAPIThemes Hook 替换它:

import { useAPIThemes } from './hooks'
  1. 然后,用以下自定义 Hook 替换useResource Hook 定义:
    const [ themes, getThemes ] = useAPIThemes()

现在ChangeTheme组件使用我们自定义的 API Hook 从 API 中获取主题。

调整注册组件

接下来,我们将通过以下步骤调整Register组件:

  1. 编辑src/user/Register.js并调整导入语句以导入useAPIRegister Hook:
import { useDispatch, useAPIRegister } from '../hooks'
  1. 然后,用以下内容替换当前的 Resource Hook:
    const [ user, register ] = useAPIRegister()

现在Register组件使用我们自定义的 API Hook 通过 API“注册”用户。

调整登录组件

Register组件类似,我们还将调整Login组件:

  1. 编辑src/user/Login.js并调整导入语句以导入useAPILogin Hook:
import { useDispatch, useAPILogin } from '../hooks'
  1. 然后,用以下内容替换当前的 Resource Hook:
    const [ user, login ] = useAPILogin()

现在Login组件使用我们自定义的 API Hook 通过 API 登录用户。

调整 CreatePost 组件

最后,我们将通过以下步骤调整CreatePost组件:

  1. 编辑src/post/CreatePost.js并调整导入语句以导入useAPICreatePost Hook:
import { useUserState, useDispatch, useAPICreatePost } from '../hooks'
  1. 然后,用以下内容替换当前的 Resource Hook:
    const [ post, createPost ] = useAPICreatePost()

现在CreatePost组件使用我们自定义的 API Hook 通过 API 创建新帖子。

使用 useDebouncedUndo Hook

最后,我们将在src/post/CreatePost.js文件中用我们自定义的useDebouncedUndo 钩子替换所有防抖撤销逻辑。这样做将使我们的组件代码更加清晰和易于阅读。此外,我们以后可以在其他组件中重用相同的防抖撤销功能。

让我们通过以下步骤在CreatePost组件中开始使用 Debounced Undo 钩子:

  1. 编辑src/post/CreatePost.js并导入useDebouncedUndo 钩子:
import { useUserState, useDispatch, useDebouncedUndo, useAPICreatePost } from '../hooks'
  1. 然后,移除与防抖撤销处理相关的以下代码:
    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()
  1. 最后,在我们的handleContent函数中移除以下设置函数(用粗体标记):
    function handleContent (e) {
        const { value } = e.target
 setInput(value)
 setDebounce(value)
    }

现在我们可以使用我们自定义钩子提供的setContent函数:

    function handleContent (e) {
        const { value } = e.target
        setContent(value)
    }

如您所见,我们的代码现在更加清晰、简洁和易于阅读。此外,我们以后可以在其他组件中重用 Debounced Undo 钩子。

示例代码

示例代码可以在Chapter10/chapter10_2文件夹中找到。

只需运行npm install来安装所有依赖项,然后运行npm start来启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

钩子之间的交互

我们整个博客应用现在的工作方式与以前相同,但它使用我们自定义的钩子!到目前为止,我们一直有封装整个逻辑的钩子,只有常量值作为参数传递给我们的自定义钩子。然而,我们也可以将其他钩子的值传递给自定义钩子!

由于钩子只是 JavaScript 函数,所有钩子都可以接受任何值作为参数并与它们一起工作:常量值、组件 props,甚至来自其他钩子的值。

我们现在要创建本地钩子,这意味着它们将放在与组件相同的文件中,因为它们在其他地方都不需要。但是,它们仍然会使我们的代码更易于阅读和维护。这些本地钩子将接受来自其他钩子的值作为参数。

以下本地钩子将被创建:

  • 本地注册效果钩子

  • 本地登录效果钩子

让我们看看如何在以下小节中创建它们。

创建本地注册效果钩子

首先,我们将从我们的Login组件中提取 Effect Hook 到一个单独的useRegisterEffect 钩子函数中。这个函数将接受来自其他钩子的以下值作为参数:userdispatch

现在让我们使用以下步骤为Register组件创建一个本地 Effect Hook:

  1. 编辑src/user/Register.js并在组件函数之外定义一个新函数,在导入语句之后:
function useRegisterEffect (user, dispatch) {
  1. 对于函数的内容,从Register组件中剪切现有的 Effect Hook,并将其粘贴在这里:
    useEffect(() => {
        if (user && user.data) {
            dispatch({ type: 'REGISTER', username: user.data.username })
        }
    }, [dispatch, user])
}
  1. 最后,定义我们的自定义useLoginEffect Hook,在其中剪切出先前的 Effect Hook,并将其他 Hooks 的值传递给它:
    useRegisterEffect(user, dispatch)

正如我们所看到的,将效果提取到一个单独的函数中使我们的代码更易于阅读和维护。

创建一个本地登录效果钩子

重复类似的过程到本地Register Effect Hook,我们还将从Login组件中提取 Effect Hook 到一个单独的useLoginEffect Hook 函数。这个函数将接受来自其他 Hooks 的以下值作为参数:userdispatchsetLoginFailed

现在让我们使用以下步骤为Login组件创建一个本地 Hook:

  1. 编辑src/user/Login.js并在组件函数之外定义一个新函数,在导入语句之后:
function useLoginEffect (user, dispatch, setLoginFailed) {
  1. 对于函数的内容,从Login组件中剪切现有的 Effect Hook,并将其粘贴在这里:
    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])
}

在这里,我们还将setLoginFailed添加到 Effect Hook 的依赖项中。这是为了确保每当setter函数发生变化时(在使用 Hook 时可能会发生),Hook 会再次触发。始终传递 Effect Hook 的所有依赖项,包括函数,可以防止以后出现错误和意外行为。

  1. 最后,定义我们的自定义useLoginEffect Hook,在其中剪切出先前的 Effect Hook,并将其他 Hooks 的值传递给它:
    useLoginEffect(user, dispatch, setLoginFailed)

正如我们所看到的,将效果提取到一个单独的函数中使我们的代码更易于阅读和维护。

示例代码

示例代码可以在Chapter10/chapter10_3文件夹中找到。

只需运行npm install来安装所有依赖项,然后运行npm start来启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

测试 Hooks

现在我们的博客应用程序充分利用了 Hooks!我们甚至为各种功能定义了自定义 Hooks,使我们的代码更具可重用性,简洁和易于阅读。

在定义自定义 Hooks 时,编写测试对它们进行测试是有意义的,以确保它们能够正常工作,即使以后我们对它们进行更改或添加更多选项。

为了测试我们的 Hooks,我们将使用 Jest 测试运行器,它包含在我们的create-react-app项目中。然而,由于 Hooks 的规则,我们不能从测试函数中调用 Hooks,因为它们只能在函数组件的主体内部调用。

因为我们不想为每个测试专门创建一个组件,我们将使用 React Hooks 测试库直接测试 Hooks。该库实际上创建一个测试组件,并提供各种实用函数来与 Hook 交互。

使用 React Hooks 测试库

除了 React Hooks 测试库,我们还需要一个专门的 React 渲染器。我们使用react-dom将 React 组件渲染到 DOM 中,而在测试中,我们可以使用react-test-renderer。现在我们将通过npm安装 React Hooks 测试库和react-test-renderer

> npm install --save-dev @testing-library/react-hooks react-test-renderer

应在以下情况下使用 React Hooks 测试库:

  • 在编写定义 Hooks 的库时

  • 当您有 Hooks 在多个组件中使用时(全局 Hooks)

然而,当一个 Hook 只在单个组件中定义和使用时(局部 Hooks),不应该使用该库。

在这种情况下,我们应该直接使用 React Testing Library 测试组件。然而,测试 React 组件超出了本书的范围。有关测试组件的更多信息可以在库网站上找到:testing-library.com/docs/react-testing-library/intro

测试简单的 Hooks

首先,我们将测试一个非常简单的 Hook,它不使用上下文或异步代码,比如超时。为了做到这一点,我们将创建一个名为useCounter的新 Hook。然后,我们将测试 Hook 的各个部分。

本节将涵盖以下任务:

  • 创建useCounter Hook

  • 测试结果

  • 测试 Hook 动作

  • 测试初始值

  • 测试重置和强制重新渲染

现在让我们开始吧。

创建useCounter Hook

useCounter Hook 将提供当前的count和用于增加重置计数器的函数。

现在让我们使用以下步骤创建useCounter Hook:

  1. 创建一个新的src/hooks/useCounter.js文件。

  2. 从 React 中导入useStateuseCallback Hooks 如下:

import { useState, useCallback } from 'react'
  1. 我们使用一个参数initialCount来定义一个新的useCounter Hook 函数:
export default function useCounter (initialCount = 0) {
  1. 然后,我们使用以下代码为count值定义一个新的 State Hook:
    const [ count, setCount ] = useState(initialCount)
  1. 接下来,我们定义增加和重置count的函数,如下所示:
    const increment = useCallback(() => setCount(count + 1), [])
    const reset = useCallback(() => setCount(initialCount), [initialCount])
  1. 最后,我们返回当前的count和两个函数:
    return { count, increment, reset }
}

现在我们已经定义了一个简单的 Hook,我们可以开始测试它。

测试 useCounter Hook 结果

现在让我们按照以下步骤为我们创建的useCounter Hook 编写测试:

  1. 创建一个新的src/hooks/useCounter.test.js文件。

  2. 从 React Hooks 测试库中导入renderHookact函数,因为我们将在稍后使用它们:

import { renderHook, act } from '@testing-library/react-hooks'
  1. 还要导入要测试的useCounter Hook,如下所示:
import useCounter from './useCounter'
  1. 现在我们可以编写我们的第一个测试。要定义一个测试,我们使用 Jest 的test函数。第一个参数是测试的名称,第二个参数是要作为测试运行的函数:
test('should use counter', () => {
  1. 在这个测试中,我们使用renderHook函数来定义我们的 Hook。这个函数返回一个带有result键的对象,其中将包含我们 Hook 的结果:
    const { result } = renderHook(() => useCounter())
  1. 现在我们可以使用 Jest 的expect来检查result对象的值。result对象包含一个current键,其中将包含来自 Hook 的当前结果:
    expect(result.current.count).toBe(0)
    expect(typeof result.current.increment).toBe('function')
})

正如我们所看到的,为 Hook 结果编写测试非常简单!创建自定义 Hook 时,特别是当它们将被公开使用时,我们应该始终编写测试以确保它们正常工作。

测试 useCounter Hook 操作

使用 React Hooks 测试库中的act函数,我们可以执行 Hook 中的函数,然后检查新的结果。

现在让我们测试我们的 Counter Hook 的操作:

  1. 按照以下代码编写一个新的test函数:
test('should increment counter', () => {
    const { result } = renderHook(() => useCounter())
  1. act函数内调用 Hook 的increment函数:
    act(() => result.current.increment())
  1. 最后,我们检查新的count现在是否为1
    expect(result.current.count).toBe(1)
})

正如我们所看到的,我们可以简单地使用act函数来触发我们的 Hook 中的操作,然后像以前一样测试值。

测试 useCounter 初始值

我们还可以在调用act之前和之后检查结果,并将初始值传递给我们的 Hook。

现在让我们测试我们的 Hook 的初始值:

  1. 定义一个新的test函数,将初始值123传递给 Hook:
test('should use initial value', () => {
    const { result } = renderHook(() => useCounter(123))
  1. 现在我们可以检查current值是否等于初始值,调用increment,并确保count从初始值增加:
    expect(result.current.count).toBe(123)
    act(() => result.current.increment())
    expect(result.current.count).toBe(124)
})

正如我们所看到的,我们可以简单地将初始值传递给 Hook,并检查值是否相同。

测试重置和强制重新渲染

现在我们要模拟组件的 props 发生变化。假设我们 Hook 的初始值是一个 prop,最初是0,然后后来变成了123。如果我们现在重置计数器,它应该重置为123而不是0。然而,为了做到这一点,我们需要在改变值后强制重新渲染我们的测试组件。

现在让我们测试重置并强制组件重新渲染:

  1. 定义test函数和一个initial值的变量:
test('should reset to initial value', () => {
    let initial = 0
  1. 接下来,我们将渲染我们的 Hook,但这次,我们还通过解构赋值取出rerender函数:
    const { result, rerender } = renderHook(() => useCounter(initial))
  1. 现在我们设置一个新的initial值并调用rerender函数:
    initial = 123
    rerender()
  1. 我们的initial值现在应该已经改变了,所以当我们调用reset时,count将被设置为123
    act(() => result.current.reset())
    expect(result.current.count).toBe(123)
})

正如我们所看到的,测试库创建了一个虚拟组件,用于测试 Hook。我们可以强制这个虚拟组件重新渲染,以模拟在真实组件中 props 发生变化时会发生什么。

测试上下文 Hooks

使用 React Hooks 测试库,我们也可以测试更复杂的 Hooks,比如使用 React 上下文的 Hooks。我们为博客应用程序创建的大多数自定义 Hooks 都使用了上下文,所以我们现在要测试这些。要测试使用上下文的 Hooks,我们首先必须创建一个上下文包装器,然后我们可以测试这个 Hook。

在这一部分,我们将执行以下操作:

  • 创建一个ThemeContextWrapper组件

  • 测试useTheme Hook

  • 创建一个StateContextWrapper组件

  • 测试useDispatch Hook

  • 测试useUserState Hook

  • 测试usePostsState Hook

让我们开始吧。

创建 ThemeContextWrapper

为了能够测试 Theme Hook,我们首先必须设置上下文并为 Hook 的测试组件提供一个包装器组件。

现在让我们创建ThemeContextWrapper组件:

  1. 创建一个新的src/hooks/testUtils.js文件。

  2. 导入ReactThemeContext,如下所示:

import React from 'react'
import { ThemeContext } from '../contexts'
  1. 定义一个名为ThemeContextWrapper的新函数组件;它将接受children作为 props:
export function ThemeContextWrapper ({ children }) {

children是 React 组件的一个特殊 prop。它将包含作为children传递给它的所有其他组件;例如,<ThemeContextWrapper>{children}</ThemeContextWrapper>

  1. 我们返回一个带有默认主题的ThemeContext.Provider,然后将children传递给它:
    return (
        <ThemeContext.Provider value={{ primaryColor: 'deepskyblue', secondaryColor: 'coral' }}>
            {children}
        </ThemeContext.Provider>
    )
}

正如我们所看到的,上下文包装器简单地返回一个上下文提供者组件。

测试 useTheme Hook

现在我们已经定义了ThemeContextWrapper组件,我们可以在测试useTheme Hook 时使用它。

现在让我们按照以下步骤测试useTheme Hook:

  1. 创建一个新的src/hooks/useTheme.test.js文件。

  2. 导入renderHook函数以及ThemeContextWrapperuseTheme Hook。

import { renderHook } from '@testing-library/react-hooks'
import { ThemeContextWrapper } from './testUtils'
import useTheme from './useTheme'
  1. 接下来,使用renderHook函数定义test,并将wrapper作为第二个参数传递给它。这样做将使用定义的wrapper组件包装测试组件,这意味着我们将能够在 Hook 中使用提供的上下文。
test('should use theme', () => {
    const { result } = renderHook(
        () => useTheme(),
        { wrapper: ThemeContextWrapper }
    )
  1. 现在我们可以检查我们的 Hook 的结果,它应该包含在ThemeContextWrapper中定义的颜色。
    expect(result.current.primaryColor).toBe('deepskyblue')
    expect(result.current.secondaryColor).toBe('coral')

正如我们所看到的,提供上下文包装器后,我们可以测试使用上下文的 Hook,就像我们测试简单的 Counter Hook 一样。

创建StateContextWrapper

对于其他使用StateContext的 Hook,我们必须定义另一个包装器来向 Hook 提供StateContext

现在让我们按照以下步骤定义StateContextWrapper组件:

  1. 编辑src/hooks/testUtils.js并调整导入语句以导入useReducer Hook,StateContextappReducer函数。
import React, { useReducer } from 'react'
import { StateContext, ThemeContext } from '../contexts'
import appReducer from '../reducers' 
  1. 定义一个名为StateContextWrapper的新函数组件。在这里,我们将使用useReducer Hook 来定义应用程序状态,这与我们在src/App.js文件中所做的类似。
export function StateContextWrapper ({ children }) {
    const [ state, dispatch ] = useReducer(appReducer, { user: '', posts: [], error: '' })
  1. 接下来,定义并返回StateContext.Provider,这与我们为ThemeContextWrapper所做的类似。
    return (
        <StateContext.Provider value={{ state, dispatch }}>
            {children}
        </StateContext.Provider>
    )
}

正如我们所看到的,创建上下文包装器总是类似的。然而,这一次,我们还在我们的包装器组件中定义了一个 Reducer Hook。

测试useDispatch Hook

现在我们已经定义了StateContextWrapper,我们可以使用它来测试useDispatch Hook。

让我们按照以下步骤测试useDispatch Hook:

  1. 创建一个新的src/hooks/useDispatch.test.js文件。

  2. 导入renderHook函数,StateContextWrapper组件和useDispatch Hook。

import { renderHook } from '@testing-library/react-hooks'
import { StateContextWrapper } from './testUtils'
import useDispatch from './useDispatch'
  1. 然后,定义test函数,将StateContextWrapper组件传递给它。
test('should use dispatch', () => {
    const { result } = renderHook(
        () => useDispatch(),
        { wrapper: StateContextWrapper }
    )
  1. 最后,检查 Dispatch Hook 的结果是否是一个函数(dispatch函数):
    expect(typeof result.current).toBe('function')
})

正如我们所看到的,使用wrapper组件总是以相同的方式工作,即使我们在wrapper组件中使用其他 Hook。

测试useUserState Hook

使用StateContextWrapper和 Dispatch Hook,我们现在可以通过派发LOGINREGISTER动作并检查结果来测试useUserState Hook。要派发这些动作,我们使用测试库中的act函数。

让我们测试useUserState Hook:

  1. 创建一个新的src/hooks/useUserState.test.js文件。

  2. 导入必要的函数,useDispatchuseUserState Hooks,以及StateContextWrapper

import { renderHook, act } from '@testing-library/react-hooks'
import { StateContextWrapper } from './testUtils'
import useDispatch from './useDispatch'
import useUserState from './useUserState'
  1. 接下来,我们编写一个测试,检查初始的user状态:
test('should use user state', () => {
    const { result } = renderHook(
        () => useUserState(),
        { wrapper: StateContextWrapper }
    )

    expect(result.current).toBe('')
})
  1. 然后,我们编写一个测试,派发一个LOGIN动作,然后检查新的状态。现在我们不再返回单个 Hook,而是返回一个包含两个 Hook 结果的对象:
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')
})
  1. 最后,我们编写一个测试,派发一个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 Hook

与我们测试useUserState Hook 的方式类似,我们也可以测试usePostsState Hook。

现在让我们测试usePostsState Hook:

  1. 创建一个新的src/hooks/usePostsState.test.js文件。

  2. 导入必要的函数,useDispatchusePostsState Hooks,以及StateContextWrapper

import { renderHook, act } from '@testing-library/react-hooks'
import { StateContextWrapper } from './testUtils'
import useDispatch from './useDispatch'
import usePostsState from './usePostsState'
  1. 然后,我们测试posts数组的初始状态:
test('should use posts state', () => {
    const { result } = renderHook(
        () => usePostsState(),
        { wrapper: StateContextWrapper }
    )

    expect(result.current).toEqual([])
})
  1. 接下来,我们测试一个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)
})
  1. 最后,我们测试一个新的帖子是否在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状态类似,但派发的动作不同。

测试异步 Hooks

有时,我们需要测试执行异步操作的 Hooks。这意味着我们需要等待一段时间,直到检查结果。为了实现这种类型的 Hooks 的测试,我们可以使用 React Hooks Testing Library 中的waitForNextUpdate函数。

在我们测试异步 Hooks 之前,我们需要了解一个叫做async/await的新 JavaScript 结构。

async/await结构

普通函数定义如下:

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 Hook 了。

测试 useDebouncedUndo Hook

我们将使用waitForNextUpdate函数来测试我们的useDebouncedUndo Hook 中的去抖动,按照以下步骤:

  1. 创建一个新的src/hooks/useDebouncedUndo.test.js文件。

  2. 导入renderHookact函数以及useDebouncedUndo Hook:

import { renderHook, act } from '@testing-library/react-hooks'
import useDebouncedUndo from './useDebouncedUndo'
  1. 首先,我们测试 Hook 是否返回正确的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)
})
  1. 接下来,我们测试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')
})

请记住,我们可以使用解构从数组中提取出的变量赋予任何名称。在这种情况下,我们首先将content变量命名为content,然后稍后将其命名为newContent

  1. 最后,我们使用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)
})

正如我们所看到的,我们可以结合waitForNextUpdate函数和async/await来轻松处理 Hooks 中的异步操作。

运行测试

要运行测试,只需执行以下命令:

> npm test

正如我们从以下截图中所看到的,所有的测试都成功通过了:

所有 Hook 测试都成功通过了

测试套件实际上会监视我们文件的更改并自动重新运行测试。我们可以使用各种命令手动触发测试重新运行,我们可以按Q退出测试运行器。

示例代码

示例代码可以在Chapter10/chapter10_4文件夹中找到。

只需运行npm install来安装所有依赖项,然后运行npm start来启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

探索 React Hooks API

官方的 React 库提供了一些内置的 Hooks,可以用来创建自定义 Hooks。我们已经了解了 React 提供的三个基本 Hooks:

  • useState

  • useEffect

  • useContext

此外,React 提供了更高级的 Hooks,在某些用例中非常有用:

  • useReducer

  • useCallback

  • useMemo

  • useRef

  • useImperativeHandle

  • useLayoutEffect

  • useDebugValue

useState Hook

useState Hook 返回一个值,该值将在重新渲染时保持不变,并返回一个更新它的函数。可以将一个值作为 initialState 的参数传递给它:

const [ state, setState ] = useState(initialState)

调用 setState 更新值并使用更新后的值重新渲染组件。如果值没有改变,React 将不会重新渲染组件。

也可以将一个函数传递给 setState 函数,第一个参数是当前值。例如,考虑以下代码:

setState(val => val + 1)

此外,如果初始状态是复杂计算的结果,可以将一个函数传递给 Hook 的第一个参数。在这种情况下,该函数只会在 Hook 初始化期间被调用一次:

const [ state, setState ] = useState(() => {
    return computeInitialState()
})

State Hook 是 React 提供的最基本和普遍的 Hook。

useEffect Hook

useEffect Hook 接受一个包含具有副作用的代码的函数,例如定时器和订阅。传递给 Hook 的函数将在渲染完成并且组件在屏幕上时运行:

useEffect(() => {
    // do something
})

一个清除函数可以从 Hook 中返回,它将在组件卸载时被调用,并且用于清除定时器或订阅等操作:

useEffect(() => {
    const interval = setInterval(() => {}, 100)
    return () => {
        clearInterval(interval)
    }
})

当 effect 的依赖项更新时,清除函数也将在触发 effect 之前被调用。

为了避免在每次重新渲染时触发 effect,我们可以将一个值数组作为 Hook 的第二个参数进行指定。只有当这些值中的任何一个发生变化时,effect 才会再次触发:

useEffect(() => {
    // do something when state changes
}, [state])

这个数组作为第二个参数传递被称为 effect 的依赖数组。如果你希望 effect 只在挂载期间触发,并且清除函数在卸载期间触发,我们可以将一个空数组作为第二个参数传递。

useContext Hook

useContext Hook 接受一个上下文对象,并返回上下文的当前值。当上下文提供程序更新其值时,Hook 将使用最新的值触发重新渲染:

const value = useContext(NameOfTheContext)

需要注意的是,上下文对象本身需要传递给 Hook,而不是消费者或提供者。

useReducer Hook

useReducer Hook 是 useState Hook 的高级版本。它接受一个 reducer 作为第一个参数,这是一个带有两个参数的函数:stateaction。然后 reducer 函数返回从当前状态和操作计算出的更新状态。如果 reducer 返回与上一个状态相同的值,React 将不会重新渲染组件或触发 effect:

const [ state, dispatch ] = useReducer(reducer, initialState, initFn)

当处理复杂的 state 变化时,我们应该使用 useReducer Hook 而不是 useState Hook。此外,处理全局 state 更容易,因为我们可以简单地传递 dispatch 函数,而不是多个 setter 函数。

dispatch 函数是稳定的,在重新渲染时不会改变,因此可以安全地从 useEffectuseCallback 的依赖中省略它。

我们可以通过设置 initialState 值或指定 initFn 函数作为第三个参数来指定初始 state。当计算初始 state 需要很长时间,或者我们想要通过 action 重用函数来重置 state 时,指定这样的函数是有意义的。

useMemo Hook

useMemo Hook 接受一个函数的结果并对其进行记忆化。这意味着它不会每次重新计算。这个 Hook 可以用于性能优化:

const memoizedVal = useMemo(
    () => computeVal(a, b, c),
    [a, b, c]
)

在前面的例子中,computeVal 是一个性能消耗较大的函数,它从 abc 计算出一个结果。

useMemo 在渲染期间运行,因此确保计算函数不会引起任何副作用,比如资源请求。副作用应该放在 useEffect Hook 中。

作为第二个参数传递的数组指定了函数的依赖项。如果这些值中的任何一个发生变化,函数将被重新计算;否则,将使用存储的结果。如果不提供数组,每次渲染都会计算一个新值。如果传递一个空数组,该值将只计算一次。

不要依赖 useMemo 只计算一次。如果长时间不使用,React 可能会忘记一些先前记忆化的值,例如为了释放内存。只用于性能优化。

useMemo Hook 用于 React 组件的性能优化。

useCallback Hook

useCallback Hook 的工作方式类似于 useMemo Hook。然而,它返回的是一个记忆化的回调函数,而不是一个值:

const memoizedCallback = useCallback(
    () => doSomething(a, b, c),
    [a, b, c]
)

前面的代码类似于以下的 useMemo Hook:

const memoizedCallback = useMemo(
    () => () => doSomething(a, b, c),
    [a, b, c]
)

返回的函数只有在第二个参数的数组中传入的依赖值发生变化时才会被重新定义。

useRef Hook

useRef Hook 返回一个 ref 对象,可以通过 ref 属性分配给组件或元素。Refs 可以用来处理 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>
}

Refs 可以用于访问 DOM,也可以用于保持可变的值,比如存储间隔的引用:

function Timer () {
    const intervalRef = useRef(null)

    useEffect(() => {
        intervalRef.current = setInterval(doSomething, 100)
        return () => clearInterval(intervalRef.current)
    })

    // ...
}

像前面的例子中使用 refs 使它们类似于类中的实例变量,比如 this.intervalRef

useImperativeHandle Hook

useImperativeHandle Hook 可以用于自定义向其他组件暴露的实例值,当将 ref 指向它时。然而,应尽量避免这样做,因为它会紧密耦合组件,从而损害可重用性。

useImperativeHandle Hook 的签名如下:

useImperativeHandle(ref, createHandle, [dependencies])

我们可以使用这个 Hook,例如暴露一个 focus 函数,其他组件可以通过对组件的 ref 触发。这个 Hook 应该与 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 Hook

useLayoutEffect Hook 与 useEffect Hook 相同,但在所有 DOM 变化完成后同步触发,并在组件在浏览器中渲染之前。它可以用于在渲染之前从 DOM 中读取信息并调整组件的外观。此 Hook 中的更新将在浏览器渲染组件之前同步处理。

除非真的需要,否则不要使用这个 Hook,这只在某些边缘情况下才需要。useLayoutEffect 会阻止浏览器的视觉更新,因此比 useEffect 更慢。

这里的规则是首先使用 useEffect。如果您的变化会改变 DOM 节点的外观,可能会导致闪烁,那么应该使用 useLayoutEffect

useDebugValue Hook

useDebugValue Hook 对于开发共享库中的自定义 Hook 非常有用。它可以用于在 React DevTools 中显示调试的特定值。

例如,在我们的 useDebouncedUndo 自定义 Hook 中,我们可以这样做:

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 Hook 将在 React DevTools 中显示以下内容:

  • 当 Hook 初始化时:DebouncedUndo:初始化

  • 当输入值时:DebouncedUndo:等待 200 毫秒

  • 防抖后(200毫秒后):DebouncedUndo:添加到历史记录

总结

在本章中,我们首先学习了如何从我们的博客应用程序中的现有代码中提取自定义 Hooks。我们将各种上下文 Hooks 提取为自定义 Hooks,然后创建 API Hooks 和用于防抖撤消功能的更高级的 Hook。接下来,我们了解了 Hooks 之间的交互以及如何在自定义 Hooks 中使用其他 Hooks 的值。然后,我们为我们的博客应用程序创建了本地 Hooks。然后,我们学习了如何使用 Jest 和 React Hooks 测试库测试各种 Hooks。最后,我们了解了在撰写时由 React Hooks API 提供的所有 Hooks。

知道何时以及如何从现有代码中提取自定义 Hooks 是 React 开发中非常重要的技能。在一个更大的项目中,我们可能会定义许多特定于项目需求的自定义 Hooks。自定义 Hooks 还可以使我们更容易地维护我们的应用程序,因为我们只需要在一个地方调整功能。测试自定义 Hooks 非常重要,因为如果以后重构我们的自定义 Hooks,我们希望确保它们仍然正常工作。现在我们知道了完整的 React Hooks API,我们可以利用 React 提供的所有 Hooks 来创建我们自己的自定义 Hooks。

在下一章中,我们将学习如何从 React 类组件迁移到基于 Hook 的系统。我们将首先使用类组件创建一个小项目,然后我们将用 Hook 替换它们,仔细研究两种解决方案之间的差异。

问题

为了总结我们在本章学到的内容,试着回答以下问题:

  1. 我们如何从现有代码中提取自定义 Hook?

  2. 创建 API Hooks 的优势是什么?

  3. 何时应该将功能提取为自定义 Hook?

  4. 我们如何使用自定义 Hooks?

  5. 何时应该创建本地 Hooks?

  6. 哪些钩子之间的交互是可能的?

  7. 我们可以使用哪个库来测试 Hooks?

  8. 我们如何测试 Hook 动作?

  9. 我们如何测试上下文?

  10. 我们如何测试异步代码?

进一步阅读

如果您对本章学到的概念更多信息感兴趣,请查看以下阅读材料: