React-挂钩学习手册-全-

React 挂钩学习手册(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

React 是一个用于构建高效和可扩展的 Web 应用程序的 JavaScript 库。React 由 Facebook 开发,被用于许多大型 Web 应用程序,如 Facebook、Instagram、Netflix 和 WhatsApp Web。

React Hooks 是在 React 16.8 版本中引入的,解决了许多 React 项目中的常见问题。Hooks 使组件变得不那么复杂,更简洁,更易于阅读和重构。此外,它们使许多 React 功能更容易使用和理解,避免了使用包装组件。

本书是学习 React Hooks 的权威指南。您将学习如何使用 React Hooks 管理 React 组件中的状态和效果,以及如何使用其他 React 功能,如上下文,通过 Hooks。通过实际示例,您将学习如何使用易于扩展和理解的代码开发大型高效的应用程序。

本书还涉及高级概念,如将 Hooks 与 Redux 和 MobX 等库结合使用。此外,您将学习如何有效地将现有项目迁移到 React Hooks。

本书适合对象

本书适用于任何级别的 JavaScript 和 React 框架的 Web 开发人员。本书还将满足那些因其先进的功能集和能力而迁移到 React 的开发人员的需求。

本书涵盖内容

第一章 介绍 React 和 React Hooks,介绍了 React 和 React Hooks 的基本原理,它们是什么以及为什么要使用它们。然后,我们通过介绍 State Hook 作为替代类组件中的 React 状态来了解 Hooks 的功能。最后,我们介绍了 React 提供的各种 Hooks,并介绍了本书中将要学习的一些 Hooks。

第二章 使用 State Hook,通过重新实现useState Hook 来深入讲解 Hook 的工作原理。通过这样做,我们发现了 Hooks 的某些限制。然后,我们将重新实现的 Hook 与真正的 Hooks 进行比较。此外,我们介绍了替代的 Hook API,并讨论了它们所面临的问题。最后,我们学习如何使用 Hooks 解决常见问题,如条件 Hooks 和循环中的 Hooks。

第三章,“使用 React Hooks 编写你的第一个应用程序”,将前两章学到的知识付诸实践,通过使用 React Hooks,特别是 State Hook,开发博客应用程序。在本章中,我们还学习了如何以可扩展的方式构建 React 项目结构。

第四章,“使用 Reducer 和 Effect Hooks”,从学习简单的 State Hook 并将其应用到实践中开始。我们将学习 React 库预定义的另外两个主要 Hooks:Reducer 和 Effect Hooks。首先我们学习何时应该使用 Reducer Hook 而不是 State Hook。然后我们学习如何将现有的 State Hook 转换为 Reducer Hook 以了解概念。最后,我们学习如何使用 Effect Hooks 实现更高级的功能。

第五章,“实现 React Context”,解释了 React 上下文以及如何在我们的应用程序中使用它。然后我们在博客应用程序中实现 React 上下文,以提供主题功能和使用 Context Hooks 的全局状态。

第六章,“实现请求和 React Suspense”,涵盖了使用 Effect Hook 和 State 或 Reducer Hook 从服务器请求资源的内容。然后我们学习如何使用React.memo来防止不必要的组件重新渲染。最后,我们了解了 React Suspense,它可以用于推迟渲染直到满足条件,也称为延迟加载。

第七章,“使用 Hooks 进行路由”,解释了如何在我们的博客应用程序中使用 Hooks 来实现路由。我们了解了 Navi,这是一个用于 React 的路由库,它利用了 Hooks 和 Suspense。我们首先在应用程序中实现页面,然后定义路由,最后开始实现路由 Hooks。

第八章《使用社区钩子》解释了 React 社区已经开发了各种利用钩子的库。在本章中,我们将学习如何实现来自社区的各种钩子,以及在哪里找到更多的钩子。我们首先学习了输入处理钩子。接下来,我们学习如何用钩子替换 React 生命周期方法。然后,我们学习了各种有用的钩子和使用钩子进行响应式设计。此外,我们学习了如何使用钩子实现撤销/重做功能。最后,我们学习了在社区提供的其他钩子的位置。

第九章《Hooks 的规则》涵盖了 Hooks 的规则。掌握 Hooks 的规则对于构建我们自己的 Hooks 非常重要,而这将在下一章中进行。我们还深入了解了 Hooks 的限制,并发现了需要注意的事项。最后,我们学习了如何使用代码检查器强制执行 Hooks 的规则。

第十章《构建自己的 Hooks》从 Hooks 的基本概念开始。我们现在将构建自己的 Hooks。我们首先从我们的博客应用程序的现有函数中提取一个自定义的 Hook,然后学习如何使用我们的自定义 Hook。接下来,我们学习了如何在 Hooks 之间传递信息。最后,我们学习了 React Hooks API 以及我们可以使用的其他 Hooks 来构建我们自己的 Hooks。在本章结束时,我们的应用程序将完全由 Hooks 驱动!

第十一章《从 React 类组件迁移》涵盖了使用 React 类组件处理状态。我们首先使用类组件实现了一个简单的 ToDo 应用程序。然后,我们学习如何将使用类组件的现有项目迁移到基于 Hooks 的实现。最后,我们学习了使用类组件与 Hooks 的权衡以及有效迁移现有项目的策略。

第十二章《Redux 和 Hooks》解释了使用 Redux 处理状态。我们首先将现有的 ToDo 应用程序迁移到 Redux,然后学习如何使用 Redux 与 Hooks。此外,我们学习了如何将现有的 Redux 应用程序迁移到 Hooks。最后,我们学习了使用 Redux 的权衡。

第十三章,MobX 和 Hooks,涵盖了使用 MobX 进行状态处理。我们首先将现有的 ToDo 应用程序迁移到 MobX。然后我们学习如何使用 Hooks 与 MobX。此外,我们还学习了如何将现有的 MobX 应用程序迁移到 Hooks。最后,我们了解了使用 MobX 的权衡之处。

充分利用本书

我们假设您已经以某种方式使用过 React,尽管本书也适合 React 的完全初学者。

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

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com

  2. 选择支持选项卡。

  3. 单击“代码下载”。

  4. 在搜索框中输入书名,并按照屏幕上的说明进行操作。

下载文件后,请确保使用最新版本的以下软件解压缩文件夹:

  • Windows 上的 WinRAR/7-Zip

  • Mac 上的 Zipeg/iZip/UnRarX

  • Linux 上的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Learn-React-Hooks。如果代码有更新,将在现有的 GitHub 存储库中进行更新。

我们还有来自丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:static.packt-cdn.com/downloads/9781838641443_ColorImages.pdf

实际操作中的代码

访问以下链接查看代码运行的视频:

bit.ly/2Mm9yoC

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL 和用户输入。以下是一个例子:“JavaScript 类提供了一个render方法,该方法返回用户界面(通常通过 JSX)。”

代码块设置如下:

class Example extends React.Component {

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

    constructor (props) {
        super(props)
        this.state = { name: '' }
        this.handleChange = this.handleChange.bind(this)
    }

任何命令行输入或输出都以以下形式书写:

> npm run-script build

粗体:表示新术语、重要单词或屏幕上看到的单词。以下是一个例子:“在本章中,我们还将学习有关JSX以及ES6引入的新 JavaScript 功能,直到ES2018。”

在代码块中,我们使用粗体格式来突出代码中的更改。通常,我们使用粗体来突出新代码。如果指定,我们可能还会使用粗体格式来指示应删除的代码部分。

警告或重要提示会出现在这样的形式中。提示和技巧会以这种形式出现。

第一部分:Hooks 简介

在本书的第一部分,我们将介绍并涵盖 React 和 React Hooks 的基础知识,包括为什么以及如何使用它们。随后,我们将在实际环境中运用所学知识,使用 React Hooks 创建一个博客应用程序。

在本节中,我们将涵盖以下章节:

  • 第一章,介绍 React 和 React Hooks

  • 第二章,使用 State Hook

  • 第三章,使用 React Hooks 编写你的第一个应用程序

第一章:介绍 React 和 React Hooks

React 是一个可以用于构建高效和可扩展 Web 应用程序的 JavaScript 库。React 由 Facebook 开发,并在许多大型 Web 应用程序中使用,如 Facebook、Instagram、Netflix 和 WhatsApp Web。

在本书中,我们将学习如何使用 React 构建复杂和高效的用户界面,同时保持代码简单和可扩展。使用 React Hooks 的新范式,我们可以极大地简化在 Web 应用程序中处理状态管理和副作用,确保以后应用程序的增长和扩展潜力。我们还将学习有关React 上下文React 悬挂,以及它们如何与 Hooks 一起使用。之后,我们将学习如何将ReduxMobX与 React Hooks 集成。最后,我们将学习如何从现有的 React 类组件、Redux 和 MobX Web 应用程序迁移到 React Hooks。

在本书的第一章中,我们将学习 React 和 React Hooks 的基本原则。我们首先学习 React 和 React Hooks 是什么,以及为什么我们应该使用它们。然后,我们继续学习 Hooks 的功能。最后,我们介绍了 React 提供的各种 Hooks 的类型,以及本书中将要学习的一些 Hooks。通过学习 React 和 React Hooks 的基础知识,我们将更好地理解本书中将介绍的概念。

本章将涵盖以下主题:

  • 了解 React 的基本原则

  • 激发对 React Hooks 的需求

  • 开始使用 React Hooks

  • 概述各种 Hooks

技术要求

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

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

查看以下视频以查看代码的实际操作:

bit.ly/2Mm9yoC

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

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

React 的原则

在我们开始学习 React Hooks 之前,我们将学习 React 的三个基本原则。这些原则使我们能够轻松编写可扩展的 Web 应用程序。了解这些基本原则很重要,因为它们将帮助我们理解 Hooks 如何以及为什么适用于 React 生态系统。

React 基于三个基本原则:

  • 声明式:我们告诉 React 我们想要它做什么,而不是告诉它如何做事情。因此,我们可以轻松设计我们的应用程序,当数据发生变化时,React 将高效地更新和渲染正确的组件。例如,下面的代码是命令式的,它是声明式的相反:
const input = ['a', 'b', 'c']
let result = []
for (let i = 0; i < input.length; i++) {
    result.push(input[i] + input[i])
}
console.log(result) // prints: [ 'aa', 'bb', 'cc' ]

正如我们所看到的,命令式代码中,我们需要一步一步地告诉计算机要做什么。然而,使用声明式代码,我们只需告诉计算机我们想要什么,如下所示:

const input = ['a', 'b', 'c']
let result = input.map(str => str + str)
console.log(result) // prints: [ 'aa', 'bb', 'cc' ]

在前面的声明式代码中,我们告诉计算机我们想要将input数组的每个元素从str映射到str + str。如我们所见,声明式代码要简洁得多。

  • 基于组件:React 封装了管理自己状态和视图的组件,然后允许我们组合它们以创建复杂的用户界面。

  • 学一次,随处编写:React 不对您的技术栈做出假设,并尽量确保您可以开发应用程序而尽量不重写现有代码。

我们刚提到 React 是基于组件的。在 React 中,有两种类型的组件:

  • 函数组件:以 props 作为参数的 JavaScript 函数,并返回用户界面(通常通过 JSX)

  • 类组件:提供render方法的 JavaScript 类,该方法返回用户界面(通常通过 JSX)

虽然函数组件更容易定义和理解,但是类组件需要处理状态、上下文和 React 的许多高级功能。然而,使用 React Hooks,我们可以处理 React 的高级功能而不需要类组件!

使用 React Hooks 的动机

React 的三个基本原则使得编写代码、封装组件和在多个平台上共享代码变得容易。React 总是尽量利用现有的 JavaScript 特性,而不是重复造轮子。因此,我们将学习软件设计模式,这些模式将适用于许多情况,而不仅仅是设计用户界面。

React 始终努力使开发者体验尽可能顺畅,同时确保保持足够的性能,而开发者不必过多担心如何优化性能。然而,在使用 React 的多年中,已经确定了一些问题。

让我们在接下来的章节中详细看看这些问题。

混乱的类

过去,我们必须使用带有特殊函数的类组件,称为生命周期方法,例如componentDidUpdate,以及特殊的状态处理方法,例如this.setState,以处理状态变化。React 类,尤其是 JavaScript 对象的this上下文,对人类和机器来说都很难阅读和理解。

this是 JavaScript 中的一个特殊关键字,它总是指向它所属的对象:

  • 在方法中,this指的是类对象(类的实例)。

  • 在事件处理程序中,this指的是接收到事件的元素。

  • 在函数或独立状态下,this指的是全局对象。例如,在浏览器中,全局对象是Window对象。

  • 在严格模式下,this在函数中是undefined

  • 此外,call()apply()等方法可以改变this所指的对象,因此它可以指向任何对象。

对于人类来说,类很难,因为this总是指向不同的东西,所以有时(例如在事件处理程序中)我们需要手动重新绑定它到类对象。对于机器来说,类很难,因为机器不知道类中的哪些方法将被调用,以及this将如何被修改,这使得优化性能和删除未使用的代码变得困难。

此外,类有时要求我们同时在多个地方编写代码。例如,如果我们想在组件渲染时获取数据,或者数据更新时获取数据,我们需要使用两种方法来做到这一点:一次在componentDidMount中,一次在componentDidUpdate中。

举个例子,让我们定义一个从应用程序编程接口API)获取数据的类组件:

  1. 首先,我们通过扩展React.Component类来定义我们的类组件:
class Example extends React.Component {
  1. 然后,我们定义componentDidMount生命周期方法,在这里我们从 API 中获取数据:
        componentDidMount () {
            fetch(`http://my.api/${this.props.name}`)
                .then(...)
        }
  1. 然而,我们还需要定义componentDidUpdate生命周期方法,以防name属性发生变化。此外,我们需要在这里添加一个手动检查,以确保只有在name属性发生变化时才重新获取数据,而不是在其他属性发生变化时:
    componentDidUpdate (prevProps) {
        if (this.props.name !== prevProps.name) {
            fetch(`http://my.api/${this.props.name}`)
                .then(...)
        }
    }
}
  1. 为了使我们的代码更少重复,我们可以定义一个名为fetchData的单独方法,以便获取我们的数据,如下所示:
        fetchData () {
            fetch(`http://my.api/${this.props.name}`)
                .then(...)
        }
  1. 然后,我们可以在componentDidMountcomponentDidUpdate中调用该方法:
        componentDidMount () {
            this.fetchData()
        }

        componentDidUpdate (prevProps) {
            if (this.props.name !== prevProps.name) {
                this.fetchData()
            }
        }

然而,即使这样,我们仍然需要在两个地方调用fetchData。每当我们更新传递给方法的参数时,我们需要在两个地方更新它们,这使得这种模式非常容易出现错误和未来的 bug。

包装地狱

在 Hooks 之前,如果我们想要封装状态管理逻辑,我们必须使用高阶组件和渲染属性。例如,我们创建一个使用上下文来处理用户认证的 React 组件如下:

  1. 我们首先通过导入authenticateUser函数来包装我们的组件与上下文,以及AuthenticationContext组件来访问上下文:
import authenticateUser, { AuthenticationContext } from './auth'
  1. 然后,我们定义了我们的App组件,在这里我们使用了AuthenticationContext.Consumer组件和user渲染属性:
const App = () => (
    <AuthenticationContext.Consumer>
        {user =>
  1. 现在,我们根据用户是否已登录来显示不同的文本:
                user ? `${user} logged in` : 'not logged in'

在这里,我们使用了两个 JavaScript 概念:

    • 一个三元运算符,它是if条件的内联版本。它看起来如下:ifThisIsTrue ? returnThis : otherwiseReturnThis
  • 一个模板字符串,它可以用来将变量插入到字符串中。它用反引号(`) 而不是普通的单引号(')。 变量可以通过${ variableName}语法插入。我们还可以在${}括号内使用任何JavaScript表达式,例如${someValue + 1}

  1. 最后,我们在用authenticateUser上下文包装后导出我们的组件:
        }
    </AuthenticationContext.Consumer>
)

export default authenticateUser(App)

在前面的示例中,我们使用了高阶的authenticateUser组件来为现有组件添加身份验证逻辑。然后,我们使用AuthenticationContext.Consumer通过其渲染属性将user对象注入到我们的组件中。

正如您可以想象的那样,使用许多上下文将导致一个庞大的树,其中有许多子树,也称为包装器地狱。例如,当我们想使用三个上下文时,包装器地狱看起来如下:

<AuthenticationContext.Consumer>
    {user => (
        <LanguageContext.Consumer>
            {language => (
                <StatusContext.Consumer>
                    {status => (
                        ...
                    )}
                </StatusContext.Consumer>
            )}
        </LanguageContext.Consumer>
    )}
</AuthenticationContext.Consumer>

这并不容易阅读或编写,如果我们以后需要更改某些内容,它也容易出错。此外,包装器地狱使得调试变得困难,因为我们必须查看一个庞大的组件树,其中许多组件仅作为包装器。

钩子来救援!

React 钩子基于与 React 相同的基本原则。它们试图通过使用现有的 JavaScript 功能来封装状态管理。因此,我们不再需要学习和理解专门的 React 功能;我们可以简单地利用我们现有的 JavaScript 知识来使用钩子。

使用钩子,我们可以解决所有前面提到的问题。我们不再需要使用类组件,因为钩子只是可以在函数组件中调用的函数。我们也不再需要为上下文使用高阶组件和渲染属性,因为我们可以简单地使用上下文钩子来获取我们需要的数据。此外,钩子允许我们在组件之间重用有状态的逻辑,而不需要创建高阶组件。

例如,上述生命周期方法的问题可以通过使用钩子来解决,如下所示:

function Example ({ name }) {
    useEffect(() => {
        fetch(`http://my.api/${this.props.name}`)
            .then(...)
    }, [ name ])
    // ...
}

这里实现的 Effect Hook 将在组件挂载时自动触发,并且每当name属性发生变化时。

此外,前面提到的包装器地狱也可以通过使用钩子来解决,如下所示:

    const user = useContext(AuthenticationContext)
    const language = useContext(LanguageContext)
    const status = useContext(StatusContext)

既然我们知道钩子可以解决哪些问题,让我们开始在实践中使用钩子吧!

开始使用 React 钩子

正如我们所见,React 钩子解决了许多问题,尤其是大型 Web 应用程序的问题。钩子是在 React 16.8 中添加的,它们允许我们使用状态以及各种其他 React 功能,而不必编写类。在本节中,我们将首先使用create-react-app初始化一个项目,然后我们将定义一个类组件,最后我们将使用钩子将同一组件编写为函数组件。在本节结束时,我们将讨论钩子的优势,以及我们如何着手迁移到基于钩子的解决方案。

create-react-app初始化项目

要初始化 React 项目,我们可以使用create-react-app工具,该工具为 React 开发设置了环境,包括以下内容:

  • Babel,以便我们可以使用 JSX 和 ES6 语法

  • 它甚至包括超出 ES6 的语言扩展,例如对象展开运算符,我们将在后面使用

  • 此外,我们甚至可以使用 TypeScript 和 Flow 语法

此外,create-react-app设置了以下内容:

  • 自动添加前缀的层叠样式表CSS),这样我们就不需要特定浏览器的-webkit等前缀

  • 一个快速的交互式单元测试运行器,带有代码覆盖报告

  • 一个实时开发服务器,它会警告我们常见的错误

  • 一个构建脚本,它为生产捆绑 JavaScript、CSS 和图像,包括哈希值和源映射

  • 一个离线优先的服务工作者和一个 Web 应用清单,以满足渐进式 Web 应用PWA)的所有标准

  • 对前面列出的所有工具的无忧更新

正如我们所见,create-react-app工具使 React 开发对我们来说变得更加容易。它是我们学习 React 以及部署 React 应用程序到生产环境的完美工具。

创建新项目

为了设置一个新项目,我们运行以下命令,该命令创建一个名为<app-name>的新目录:

> npx create-react-app <app-name>

如果你更喜欢使用yarn包管理器,你可以运行yarn create react-app <app-name>来代替。

我们现在将使用create-react-app创建一个新项目。运行以下命令以创建第一个章节中的第一个示例的新 React 项目:

> npx create-react-app chapter1_1

既然我们已经初始化了项目,让我们继续启动项目。

启动项目

为了在开发模式下启动项目,我们必须运行npm start命令。运行以下命令:

> npm start

现在,我们可以通过在浏览器中打开http://localhost:3000来访问我们的项目:

我们的第一个 React 应用!

正如我们所见,使用create-react-app,设置一个新的 React 项目相当容易!

部署项目

为了构建用于生产部署的项目,我们只需运行build脚本:

  1. 运行以下命令以构建用于生产部署的项目:
> npm run-script build

使用yarn,我们可以简单地运行yarn build。实际上,我们可以以这种方式运行任何不与内部yarn命令名称冲突的包脚本:yarn <script-name>,而不是npm run-script <script-name>

  1. 然后,我们可以使用 Web 服务器或使用serve工具来提供我们的静态构建文件夹。首先,我们必须安装它:
> npm install -g serve
  1. 然后,我们可以运行以下serve命令:
> serve -s build

serve命令的-s标志将所有未找到的请求重写为index.html,允许客户端路由。

现在,我们可以通过在浏览器中打开http://localhost:5000来访问同一个应用。请注意,serve工具不会自动在您的浏览器中打开页面。

在了解了create-react-app之后,我们现在将用 React 编写我们的第一个组件。

从类组件开始

首先,我们从传统的 React 类组件开始,它允许我们输入一个名字,然后我们在我们的应用中显示这个名字。

设置项目

如前所述,我们将使用create-react-app来初始化我们的项目。如果你还没有这样做,现在运行以下命令:

> npx create-react-app chapter1_1

接下来,我们将把我们的应用定义为类组件。

定义类组件

我们首先将我们的应用编写为传统的类组件,如下所示:

  1. 首先,我们从src/App.js文件中删除所有代码。

  2. 接下来,在src/App.js中,我们导入React

import React from 'react'     
  1. 然后,我们开始定义我们自己的类组件——MyName
class MyName extends React.Component {
  1. 接下来,我们必须定义一个constructor方法,在其中设置初始的state对象,这将是一个空字符串。在这里,我们还需要确保调用super(props),以便让React.Component构造函数知道props对象:
    constructor (props) {
        super(props)
        this.state = { name: '' }
    }
  1. 现在,我们定义一个方法来设置name变量,使用this.setState。由于我们将使用这个方法来处理来自文本字段的输入,我们需要使用evt.target.value来从输入字段获取值:
   handleChange (evt) {
       this.setState({ name: evt.target.value })
   }
  1. 然后,我们定义render方法,在其中我们将显示一个输入字段和名字:
   render () {
  1. 为了从this.state对象中获取name变量,我们将使用解构:
       const { name } = this.state

前面的语句等同于做以下操作:

       const name = this.state.name
  1. 然后,我们显示当前输入的name状态变量:
    return (
        <div>
            <h1>My name is: {name}</h1>
  1. 我们显示一个input字段,将处理方法传递给它:
                <input type="text" value={name} onChange={this.handleChange} />
            </div>
        )
    }
}
  1. 最后,我们导出我们的类组件:
export default MyName

如果我们现在运行这段代码,当我们输入文本时,我们会得到以下错误,因为将处理方法传递给onChange改变了this上下文:

未捕获的 TypeError:无法读取未定义的属性'setState'

  1. 所以,现在我们需要调整constructor方法并重新绑定我们处理方法的this上下文到类:
    constructor (props) {
        super(props)
        this.state = { name: '' }
        this.handleChange = this.handleChange.bind(this)
    }

有可能使用箭头函数作为类方法,以避免重新绑定this上下文。然而,为了使用这个特性,我们需要安装 Babel 编译器插件,@babel/plugin-proposal-class-properties,因为它还不是已发布的 JavaScript 特性。

最后,我们的组件工作了!如你所见,为了使状态处理在类组件中正常工作,需要编写大量的代码。我们还需要重新绑定this上下文,否则我们的处理方法将无法工作。这并不直观,而且在开发过程中很容易忽略,导致令人讨厌的开发体验。

示例代码

示例代码可以在Chapter01/chapter1_1文件夹中找到。

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

使用 Hooks 替代

使用传统的类组件编写我们的应用之后,我们将使用 Hooks 来编写相同的应用。和之前一样,我们的应用将允许我们输入一个名字,然后在我们应用中显示这个名字。

请注意,只有在 React 函数组件中才能使用 Hooks。你不能在 React 类组件中使用 Hooks!

现在,我们开始设置项目。

设置项目

再次,我们使用create-react-app来设置我们的项目:

> npx create-react-app chapter1_2

现在让我们开始使用 Hooks 定义函数组件。

定义函数组件

现在,我们将同一个组件定义为函数组件:

  1. 首先,我们从src/App.js文件中删除所有代码。

  2. 接下来,在src/App.js中,我们导入 React 和useState Hook:

    import React, { useState } from 'react'
  1. 我们从函数定义开始。在我们的例子中,我们没有传递任何参数,因为我们的组件没有任何 props:
    function MyName () {

下一步将是从组件状态中获取name变量。但是,我们不能在函数组件中使用this.state。我们已经了解到 Hooks 只是 JavaScript 函数,但这究竟意味着什么?这意味着我们可以像使用任何其他 JavaScript 函数一样,直接从函数组件中使用 Hooks!

通过 Hooks 使用状态,我们调用useState()函数,并将初始状态作为参数传递。该函数返回一个包含两个元素的数组:

    • 当前状态

    • 设置状态的 setter 函数

  1. 我们可以使用解构来将这两个元素分别存储在单独的变量中,如下所示:
            const [ name, setName ] = useState('')

前面的代码等同于以下代码:

            const nameHook = useState('')
            const name = nameHook[0]
            const setName = nameHook[1]
  1. 现在,我们定义输入处理函数,在其中我们使用setName setter 函数:
            function handleChange (evt) {
                setName(evt.target.value)
            }

由于我们现在不处理类,因此不再需要重新绑定this了!

  1. 最后,我们通过从函数返回它来渲染我们的用户界面。然后,我们导出函数组件:
    return (
        <div>
            <h1>My name is: {name}</h1>
            <input type="text" value={name} onChange={handleChange} />
        </div>
    )
}

export default MyName

就这样——我们第一次成功地使用了 Hooks!如您所见,useState Hook 是this.statethis.setState的直接替代品。

让我们通过执行npm start来运行我们的应用,并在浏览器中打开http://localhost:3000

我们的第一个使用 Hooks 的 React 应用

在实现同一个应用的类组件和函数组件之后,让我们比较解决方案。

示例代码

示例代码可以在Chapter01/chapter1_2文件夹中找到。

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

比较解决方案

让我们比较我们的两个解决方案,以便看到类组件和使用 Hooks 的函数组件之间的差异。

类组件

类组件使用constructor方法来定义状态,并且需要重新绑定this以便将处理程序方法传递给input字段。完整的类组件代码如下所示:

import React from 'react'

class MyName extends React.Component {
    constructor (props) {
        super(props)
        this.state = { name: '' }

        this.handleChange = this.handleChange.bind(this)
    }

    handleChange (evt) {
        this.setState({ name: evt.target.value })
    }

    render () {
        const { name } = this.state
        return (
            <div>
                <h1>My name is: {name}</h1>
                <input type="text" value={name} onChange={this.handleChange} />
            </div>
        )
    }
}

export default MyName

正如我们所见,类组件需要大量的样板代码来初始化state对象和处理函数。

现在,让我们来看一下函数组件。

使用 Hook 的函数组件

函数组件使用useState Hook,因此我们不需要处理thisconstructor方法。完整的函数组件代码如下所示:

import React, { useState } from 'react'

function MyName () {
    const [ name, setName ] = useState('')

    function handleChange (evt) {
        setName(evt.target.value)
    }

    return (
        <div>
            <h1>My name is: {name}</h1>
            <input type="text" value={name} onChange={handleChange} />
        </div>
    )
}

export default MyName

正如我们所见,钩子使我们的代码更加简洁,更容易推理。我们不再需要担心内部工作原理;我们可以简单地通过访问useState函数来使用状态!

钩子的优势

让我们回顾一下 React 的第一原则:

声明性:我们不是告诉 React 如何做事情,而是告诉它我们想要它做什么。因此,我们可以轻松设计我们的应用程序,而 React 将有效地更新和渲染数据变化时恰好需要的组件。

正如我们在本章中学到的,钩子允许我们编写告诉 React 我们想要什么的代码。然而,使用类组件时,我们需要告诉 React 如何做事情。因此,钩子比类组件更具声明性,使它们更适合 React 生态系统。

钩子因其声明性,使得 React 能够对我们的代码进行各种优化,因为分析函数和函数调用比分析类及其复杂的this行为更容易。此外,钩子使得抽象和在组件之间共享常见的有状态逻辑变得更加容易。通过使用钩子,我们可以避免使用渲染属性和高阶组件。

我们可以看到,钩子不仅使我们的代码更加简洁,而且对开发者来说更容易推理,它们还使代码更容易为 React 优化。

迁移到钩子

现在,您可能会想:这是否意味着类组件已经过时,我们现在需要将所有内容迁移到钩子?当然不是——钩子是完全可选的。您可以在一些组件中尝试钩子,而不需要重写任何其他代码。React 团队目前也没有计划删除类组件。

现在不必急于将所有内容迁移到钩子。建议您在某些组件中逐步采用钩子,这些组件将最有用。例如,如果您有许多处理类似逻辑的组件,您可以将逻辑提取到钩子中。您还可以在类组件旁边使用带有钩子的函数组件。

此外,钩子是 100%向后兼容的,并提供了一个直接的 API,用于您已经了解的所有 React 概念:propsstatecontextrefs生命周期。此外,钩子提供了新的方式来组合这些概念,并以一种不会导致包装器地狱或类似问题的方式更好地封装它们的逻辑。我们将在本书后面了解更多关于这方面的内容。

钩子的思维方式

钩子的主要目标是解耦有状态逻辑和渲染逻辑。它们允许我们在单独的函数中定义逻辑并在多个组件中重用它们。使用钩子,我们不需要为了实现有状态逻辑而改变我们的组件层次结构。不再需要定义一个单独的组件来为多个组件提供状态逻辑,我们可以简单地使用一个钩子!

然而,Hooks 需要与经典 React 开发完全不同的思维方式。我们不应该再考虑组件的生命周期。相反,我们应该考虑数据流。例如,我们可以告诉 Hooks 在某些 props 或其他 Hooks 的值发生变化时触发。我们将在第四章《使用 Reducer 和 Effect Hooks》中学习更多关于这个概念的内容。我们也不应该再根据生命周期来拆分组件。相反,我们可以使用 Hooks 来处理常见的功能,如获取数据或设置订阅。

Hooks 的规则

Hooks 非常灵活。然而,使用 Hooks 存在一定的限制,我们应该始终牢记:

  • Hooks 只能用于函数组件,不能用于类组件

  • Hook 定义的顺序很重要,需要保持不变;因此,我们不能将 Hooks 放在 if 条件语句、循环或嵌套函数中

我们将在本书中更详细地讨论这些限制,以及如何绕过它们。

各种 Hooks 的概述

正如我们在上一节中学到的,Hooks 提供了直接访问所有 React 概念的 API。此外,我们可以定义自己的 Hooks,以便在不编写高阶组件的情况下封装逻辑,从而避免包装器地狱。在本节中,我们将概述将在本书中学习的各种 Hooks。

React 提供的 Hooks

React 已经为不同的功能提供了各种 Hooks。有三个基本 Hooks 和一些额外的 Hooks。

基本 Hooks

基本 Hooks 提供了有状态 React 应用中最常用的功能。它们如下:

  • useState

  • useEffect

  • useContext

让我们在接下来的章节中逐一了解这些内容。

useState

我们已经使用过这个 Hook。它返回一个有状态的值(state)和一个设置函数(setState)以便更新值。

useState Hook 用于处理 React 中的 state。我们可以这样使用它:

import { useState } from 'react'

const [ state, setState ] = useState(initialState)

useState Hook 取代了 this.statethis.setState()

useEffect

这个 Hook 的工作方式类似于在 componentDidMountcomponentDidUpdate 上添加一个函数。此外,Effect Hook 允许从中返回一个清理函数,其工作方式类似于在 componentWillUnmount 上添加一个函数。

useEffect Hook 用于处理有副作用的代码,如定时器、订阅、请求等。我们可以这样使用它:

import { useEffect } from 'react'

useEffect(didUpdate)

useEffect Hook 取代了 componentDidMountcomponentDidUpdatecomponentWillUnmount 方法。

useContext

这个 Hook 接受一个上下文对象并返回当前的上下文值。

useContext Hook 用于处理 React 中的上下文。我们可以这样使用它:

import { useContext } from 'react'

const value = useContext(MyContext)

useContext Hook 取代了上下文消费者。

额外的 Hooks

额外的 Hooks 要么是基本 Hooks 的更通用变体,要么是为某些边缘情况所需的。我们将要查看的额外 Hooks 如下:

  • useRef

  • useReducer

  • useMemo

  • useCallback

  • useLayoutEffect

  • useDebugValue

让我们在以下部分中深入研究这些额外的钩子。

useRef

此钩子返回一个可变的 ref 对象,其中 .current 属性被初始化为传入的参数(initialValue)。我们可以这样使用它:

import { useRef } from 'react'

const refContainer = useRef(initialValue)

useRef 钩子用于处理 React 中元素和组件的引用。我们可以通过将 ref 属性传递给元素或组件来设置引用,如下所示:<ComponentName ref={refContainer} />

useReducer

此钩子是 useState 的替代品,与 Redux 库的工作方式类似。我们可以这样使用它:

import { useReducer } from 'react'

const [ state, dispatch ] = useReducer(reducer, initialArg, init)

useReducer 钩子用于处理复杂的状态逻辑。

useMemo

记忆化是一种优化技术,其中函数调用的结果被缓存,然后在再次出现相同输入时返回。useMemo 钩子允许我们计算一个值并将其记忆化。我们可以这样使用它:

import { useMemo } from 'react'

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

useMemo 钩子在避免重新执行昂贵操作时非常有用,有助于优化。

useCallback

此钩子允许我们传递内联回调函数和依赖项数组,并返回回调函数的记忆化版本。我们可以这样使用它:

import { useCallback } from 'react'

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

useCallback 钩子在将回调传递给优化的子组件时非常有用。它与 useMemo 钩子类似,但对于回调函数。

useLayoutEffect

这个钩子与 useEffect 相同,但它只在所有 文档对象模型DOM)突变后触发。我们可以这样使用它:

import { useLayoutEffect } from 'react'

useLayoutEffect(didUpdate)

useLayoutEffect 钩子可用于读取 DOM 信息。

尽可能使用 useEffect 钩子,因为 useLayoutEffect 会阻止视觉更新并减慢应用程序速度。

最后,我们将研究在撰写本文时由 React 提供的最后一个钩子。

useDebugValue

此钩子可用于在创建自定义钩子时在 React DevTools 中显示标签。我们可以这样使用它:

import { useDebugValue } from 'react'

useDebugValue(value)

确保在自定义钩子中使用此钩子以显示钩子的当前状态,因为它将使调试它们变得更加容易。

Community Hooks

除了 React 提供的所有钩子之外,社区已经发布了许多库。这些库也提供钩子。我们将要研究的钩子如下:

  • useInput

  • useResource

  • useDimensions

  • Navigation Hooks

  • 生命周期钩子

  • Timer Hooks

让我们在以下部分中概述这些钩子是什么。

useInput

此钩子用于轻松实现输入处理,并将 input 字段的状态与变量同步。它可以这样使用:

import { useInput } from 'react-hookedup'

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

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

如我们所见,钩子极大地简化了在 React 中处理输入字段的过程。

useResource

此钩子可用于通过请求在我们的应用程序中实现异步数据加载。我们可以这样使用它:

import { useRequest } from 'react-request-hook'

const [profile, getProfile] = useResource(id => ({
    url: `/user/${id}`,
    method: 'GET'
})

如我们所见,使用专门处理获取数据的钩子非常简单。

Navigation Hooks

这些钩子是 Navi 库的一部分,用于在 React 中通过钩子实现路由。Navi 库提供了许多与路由相关的钩子。我们将在本书后面深入学习通过钩子进行路由。我们可以这样使用它们:

import { useCurrentRoute, useNavigation } from 'react-navi'

const { views, url, data, status } = useCurrentRoute()
const { navigate } = useNavigation()

正如我们所见,钩子使得路由处理变得更加容易。

生命周期钩子

react-hookedup库提供了各种钩子,包括 React 的所有生命周期监听器。

请注意,在使用钩子开发时不建议从组件生命周期的角度思考。这些钩子只是提供了一种快速重构现有组件到钩子的方法。然而,在开发新组件时,建议你考虑数据流和依赖关系,而不是生命周期。

这里我们列出了两个,但实际上库中提供了更多的钩子,我们将在后面学习。我们可以这样使用react-hookedup提供的钩子:

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

useOnMount(() => { ... })
useOnUnmount(() => { ... })

正如我们所见,钩子可以直接替换类组件中的生命周期方法。

计时器钩子

react-hookedup库还提供了用于setIntervalsetTimeout的钩子。这些钩子的工作方式类似于直接调用setTimeoutsetInterval,但作为 React 钩子,它将在重新渲染之间保持持久性。如果我们直接在函数组件中定义计时器而没有使用钩子,我们将在每次组件重新渲染时重置计时器。

我们可以将毫秒数作为第二个参数传递。我们可以这样使用它们:

import { useInterval, useTimeout } from 'react-hookedup'

useInterval(() => { ... }, 1000)
useTimeout(() => { ... }, 1000)

正如我们所看到的,Hooks 极大地简化了我们在 React 中处理间隔和超时的方式。

其他社区 Hooks

正如你可以想象的那样,社区提供了许多其他的 Hooks。我们将深入学习之前提到的社区 Hooks,以及第八章中的其他社区 Hooks:使用社区 Hooks

总结

在本书的第一章中,我们首先学习了 React 的基本原则以及它提供的组件类型。然后,我们继续学习了类组件的常见问题,以及如何使用 React 的现有功能,以及它们如何违反基本原则。接下来,我们使用类组件和带有 Hooks 的函数组件实现了一个简单的应用程序,以便能够比较这两种解决方案之间的差异。正如我们所发现的,带有 Hooks 的函数组件更适合 React 的基本原则,因为它们不会遇到类组件的相同问题,并且使我们的代码更加

简洁易懂!最后,我们初次见识了本书中将要学习的各种 Hooks。在本章之后,React 和 React Hooks 的基础知识已经清晰。现在我们可以继续学习更高级的 Hooks 概念。

在下一章中,我们将深入了解 State Hook 的工作原理,通过从头开始重新实现它。通过这样做,我们将了解 Hooks 的内部工作原理以及它们的局限性。之后,我们将使用 State Hook 创建一个小型的博客应用程序!

问题

回顾一下我们在本章学到的内容,尝试回答以下问题:

  1. React 的三个基本原则是什么?

  2. React 中有哪两种类型的组件?

  3. React 中类组件存在哪些问题?

  4. 在 React 中使用高阶组件的问题是什么?

  5. 我们可以使用哪个工具来设置一个 React 项目,我们需要运行什么命令来使用它?

  6. 如果我们在类组件中遇到以下错误,我们需要做什么:TypeError: undefined is not an object (evaluating 'this.setState')

  7. 我们如何使用 Hooks 访问和设置 React 状态?

  8. 使用 Hooks 的函数组件与类组件相比有什么优势?

  9. 在更新 React 时,我们是否需要使用 Hooks 替换所有类组件为函数组件?

  10. React 提供的三个基本 Hooks 是什么?

进一步阅读

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

第二章:使用 State Hook

现在你已经了解了 React 的原则,并且对 Hooks 有了介绍,我们将深入学习 State Hook。我们将首先通过重新实现来学习 State Hook 的内部工作原理。接下来,我们将了解 Hooks 的一些限制以及它们存在的原因。然后,我们将学习可能的替代 Hook API 及其相关问题。最后,我们将学习如何解决由 Hooks 限制导致的常见问题。通过本章的学习,我们将知道如何使用 State Hook 来实现 React 中的有状态函数组件。

本章将涵盖以下主题:

  • useState Hook 重新实现为一个简单的函数,用于访问全局状态

  • 将我们的重新实现与真实的 React Hooks 进行比较,并了解它们之间的区别

  • 学习可能的替代 Hook API 及其权衡

  • 解决由 Hooks 限制导致的常见问题

  • 解决条件 Hooks 的问题

技术要求

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

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

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

bit.ly/2Mm9yoC

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

现在,让我们开始本章的学习。

重新实现 useState 函数

为了更好地理解 Hooks 的内部工作原理,我们将从头开始重新实现useState Hook。但是,我们不会将其实现为实际的 React Hook,而是作为一个简单的 JavaScript 函数——只是为了了解 Hooks 实际在做什么。

请注意,这个重新实现并不完全是 React Hooks 内部的工作原理。实际的实现是类似的,因此具有类似的约束。然而,真实的实现要比我们在这里实现的要复杂得多。

我们现在将开始重新实现 State Hook:

  1. 首先,我们从chapter1_2中复制代码,我们将用我们自己的实现替换当前的useState Hook。

  2. 打开src/App.js并通过删除以下行来移除 Hook 的导入:

import  React,  {  useState  }  from  'react' 

用以下代码替换它:

import  React  from  'react'
import ReactDOM from 'react-dom'

我们将需要ReactDOM来强制重新渲染我们的useState Hook 的组件。如果我们使用实际的 React Hooks,这将在内部处理。

  1. 现在,我们定义我们自己的useState函数。正如我们已经知道的,useState函数将initialState作为参数:
function useState (initialState) {
  1. 然后,我们定义一个值,我们将在其中存储我们的状态。起初,这个值将被设置为传递给函数的initialState
    let value = initialState
  1. 接下来,我们定义setState函数,我们将在其中将值设置为不同的东西,并强制重新渲染我们的MyName组件:
    function setState (nextValue) {
        value = nextValue
        ReactDOM.render(<MyName />, document.getElementById('root'))
    }
  1. 最后,我们将valuesetState函数作为数组返回:
    return [ value, setState ]
}

我们使用数组而不是对象的原因是,我们通常想要重命名valuesetState变量。使用数组使得通过解构很容易重命名变量:

const [ name, setName ] = useState('')

正如我们所看到的,Hooks 是处理副作用的简单的 JavaScript 函数,比如设置有状态的值。

我们的 Hook 函数使用闭包来存储当前值。闭包是一个环境,变量存在并被存储在其中。在我们的例子中,函数提供了闭包,value变量被存储在闭包中。setState函数也在同一个闭包中定义,这就是为什么我们可以在该函数中访问value变量。在useState函数之外,我们不能直接访问value变量,除非我们从函数中返回它。

我们简单的 Hook 实现存在的问题

如果我们现在运行我们的 Hook 实现,我们会注意到当我们的组件重新渲染时,状态被重置,所以我们无法在字段中输入任何文本。这是因为每次我们的组件重新渲染时value变量的重新初始化,这是因为我们每次渲染组件时都调用useState

在接下来的部分,我们将通过使用全局变量来解决这个问题,然后将简单值转换为数组,从而允许我们定义多个 Hooks。

使用全局变量

正如我们所学的,值存储在由 useState 函数定义的闭包中。每次组件重新渲染时,闭包都会被重新初始化,这意味着我们的值将被重置。为了解决这个问题,我们需要将值存储在函数之外的全局变量中。这样,value 变量将在函数之外的闭包中,这意味着当函数再次被调用时,闭包不会被重新初始化。

我们可以定义一个全局变量如下:

  1. 首先,在 useState 函数定义之前,我们添加以下行(加粗)。
let value

function useState (initialState) {
  1. 然后,我们用以下代码替换我们函数中的第一行:
       if (typeof value === 'undefined') value = initialState

现在,我们的 useState 函数使用全局 value 变量,而不是在其闭包中定义 value 变量,因此当函数再次被调用时,它不会被重新初始化。

定义多个 Hook

我们的 Hook 函数起作用了!但是,如果我们想要添加另一个 Hook,我们会遇到另一个问题:所有的 Hook 都写入同一个全局 value 变量!

让我们通过向我们的组件添加第二个 Hook 来更仔细地研究这个问题。

向我们的组件添加多个 Hook

假设我们想要为用户的姓氏创建第二个字段,如下所示:

  1. 我们首先在函数开头创建一个新的 Hook,放在当前 Hook 之后:
    const [ name, setName ] = useState('')
 const [ lastName, setLastName ] = useState('')
  1. 然后,我们定义另一个 handleChange 函数:
    function handleLastNameChange (evt) {
        setLastName(evt.target.value)
    }
  1. 接下来,我们将 lastName 变量放在名字后面:
 <h1>My name is: {name} **{lastName}**</h1>
  1. 最后,我们添加另一个 input 字段:
            <input type="text" value={lastName} onChange={handleLastNameChange}
   />

当我们尝试这样做时,我们会注意到我们重新实现的 Hook 函数同时使用相同的值,所以我们总是同时更改两个字段。

实现多个 Hook

为了实现多个 Hook,我们应该有一个 Hook 值的数组,而不是一个单一的全局变量。

现在,我们将 value 变量重构为 values 数组,以便我们可以定义多个 Hook:

  1. 删除以下代码行:
let value

用以下代码片段替换它:

let values = []
let currentHook = 0
  1. 然后,编辑 useState 函数的第一行,我们现在在 values 数组的 currentHook 索引处初始化值:
    if (typeof values[currentHook] === 'undefined') values[currentHook] = initialState
  1. 我们还需要更新 setter 函数,以便只更新相应的状态值。在这里,我们需要将currentHook的值存储在单独的hookIndex变量中,因为currentHook的值稍后会更改。这确保在useState函数的闭包中创建了currentHook变量的副本。否则,useState函数将访问外部闭包中的currentHook变量,该变量在每次调用useState时都会被修改。
    let hookIndex = currentHook
    function setState (nextValue) {
        values[hookIndex] = nextValue
        ReactDOM.render(<MyName />, document.getElementById('root'))
    }
  1. 编辑useState函数的最后一行,如下所示:
        return [ values[currentHook++], setState ]

使用values[currentHook++],我们将currentHook的当前值作为索引传递给values数组,然后将currentHook增加一。这意味着在从函数返回后currentHook将增加。

如果我们想先增加一个值,然后再使用它,我们可以使用arr[++indexToBeIncremented]语法,它首先增加,然后将结果传递给数组。

  1. 当我们开始渲染组件时,我们仍然需要重置currentHook计数器。在组件定义之后添加以下行(用粗体标出):
function Name () {
    currentHook = 0

最后,我们对useState Hook 的简单重新实现有效!以下截图突出显示了这一点:

我们的自定义 Hook 重新实现有效

正如我们所看到的,使用全局数组来存储我们的 Hook 值解决了在定义多个 Hook 时遇到的问题。

示例代码

简单 Hook 重新实现的示例代码可以在Chapter02/chapter2_1文件夹中找到。

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

我们可以定义条件 Hook 吗?

如果我们想要添加一个复选框来切换使用名字字段,该怎么办?

让我们通过实现这样一个复选框来找出:

  1. 首先,我们添加一个新的 Hook 来存储复选框的状态:
    const [ enableFirstName, setEnableFirstName ] = useState(false)
  1. 然后,我们定义一个处理函数:
 function  handleEnableChange  (evt)  { setEnableFirstName(!enableFirstName) }
  1. 接下来,我们渲染一个复选框:
            <input type="checkbox" value={enableFirstName} onChange={handleEnableChange} />
  1. 如果我们不想显示名字,可以编辑以下现有行以添加对enableFirstName变量的检查:
            <h1>My name is: {enableFirstName ? name : ''} {lastName}</h1>
  1. 我们是否可以将 Hook 定义放入if条件或三元表达式中,就像我们在以下代码片段中所做的那样?
    const [ name, setName ] = enableFirstName
        ? useState('')
        : [ '', () => {} ]
  1. 实际上,最新版本的react-scripts在定义条件钩子时会抛出错误,因此我们需要通过运行以下命令来降级库以进行示例:
> npm install --save react-scripts@².1.8

在这里,我们要么使用钩子,要么如果名字被禁用,我们返回初始状态和一个空的 setter 函数,这样编辑输入字段就不起作用。

如果我们现在尝试运行这段代码,我们会注意到编辑姓氏仍然有效,但编辑名字不起作用,这正是我们想要的。正如我们在以下截图中所看到的,现在只有编辑姓氏有效:

勾选复选框之前的应用状态

当我们点击复选框时,会发生一些奇怪的事情:

  • 复选框已被选中

  • 名字输入字段已启用

  • 现在姓氏字段的值是名字字段的值

我们可以在以下截图中看到单击复选框的结果:

勾选复选框后的应用状态

我们可以看到姓氏状态现在在名字字段中。值已经交换,因为钩子的顺序很重要。正如我们从我们的实现中所知,我们使用currentHook索引来知道每个钩子的状态存储在哪里。然而,当我们在两个现有钩子之间插入一个额外的钩子时,顺序就会混乱。

在勾选复选框之前,values数组如下:

  • [false, '']

  • 钩子顺序:enableFirstNamelastName

然后,我们在lastName字段中输入了一些文本:

  • [false, 'Hook']

  • 钩子顺序:enableFirstNamelastName

接下来,我们切换复选框,激活了我们的新钩子:

  • [true, 'Hook', '']

  • 钩子顺序:enableFirstNamenamelastName

正如我们所看到的,在两个现有钩子之间插入一个新的钩子会使name钩子窃取下一个钩子(lastName)的状态,因为它现在具有与lastName钩子先前相同的索引。现在,lastName钩子没有值,这导致它设置初始值(空字符串)。因此,切换复选框会将lastName字段的值放入name字段中。

示例代码

我们简单的钩子重新实现的条件钩子问题的示例代码可以在Chapter02/chapter2_2文件夹中找到。

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

将我们的重新实现与真实的 Hooks 进行比较

我们简单的 Hook 实现已经让我们对 Hooks 内部工作原理有了一些了解。然而,在现实中,Hooks 并不使用全局变量。相反,它们在 React 组件内部存储状态。它们还在内部处理 Hook 计数器,因此我们不需要在函数组件中手动重置计数。此外,当状态改变时,真正的 Hooks 会自动触发我们组件的重新渲染。然而,为了能够做到这一点,Hooks 需要从 React 函数组件中调用。React Hooks 不能在 React 之外或在 React 类组件内部调用。

通过重新实现useState Hook,我们学到了一些东西:

  • Hooks 只是访问 React 功能的函数

  • Hooks 处理持续存在于重新渲染中的副作用

  • Hook 定义的顺序很重要

最后一点尤其重要,因为这意味着我们不能有条件地定义 Hooks。我们应该始终在函数组件的开头定义所有的 Hook,并且永远不要在if或其他结构中嵌套它们。

在这里,我们还学到了以下内容:

  • React Hooks 需要在 React 函数组件内部调用

  • React Hooks 不能有条件地定义,也不能在循环中定义

我们现在将看一下允许有条件 Hooks 的替代 Hook API。

替代 Hook API

有时,有条件地或在循环中定义 Hooks 可能会很好,但为什么 React 团队决定这样实现 Hooks 呢?有什么替代方案吗?让我们来看看其中的一些。

命名的 Hooks

我们可以给每个 Hook 一个名称,然后将 Hooks 存储在对象中,而不是数组中。然而,这不会产生一个好的 API,并且我们还必须考虑想出唯一的 Hook 名称:

// NOTE: Not the actual React Hook API
const [ name, setName ] = useState('nameHook', '')

此外,当条件设置为false时,或者从循环中移除一个项目时会发生什么?我们会清除 Hook 状态吗?如果我们不清除 Hook 状态,可能会导致内存泄漏。

即使我们解决了所有这些问题,仍然会存在名称冲突的问题。例如,如果我们创建一个自定义钩子,利用了useState钩子,并将其命名为nameHook,那么我们在组件中就不能再调用任何其他钩子nameHook,否则就会造成名称冲突。这甚至适用于来自库的钩子名称,因此我们需要确保避免与库定义的钩子发生名称冲突!

钩子工厂

或者,我们也可以创建一个钩子工厂函数,它在内部使用Symbol,以便为每个钩子提供一个唯一的键名:

function createUseState () {
    const keyName = Symbol()

    return function useState () {
        // ... use unique key name to handle hook state ...
    }
}

然后,我们可以按照以下方式使用工厂函数:

// NOTE: Not the actual React Hook API
const useNameState = createUseState()

function MyName () {
    const [ name, setName ] = useNameState('')
    // ...
}

然而,这意味着我们需要实例化每个钩子两次:一次在组件外部,一次在函数组件内部。这会增加出错的可能性。例如,如果我们创建两个钩子并复制粘贴样板代码,那么我们可能会在使用工厂函数生成的钩子名称时出错,或者在组件内部使用钩子时出错。

这种方法还使得创建自定义钩子变得更加困难,迫使我们编写包装函数。此外,调试这些包装函数比调试简单函数更加困难。

其他替代方案

对于 React Hooks,有许多提出的替代 API,但它们每个都遇到了类似的问题:要么使 API 更难使用,更难调试,要么引入了名称冲突的可能性。

最终,React 团队决定,最简单的 API 是通过计算调用它们的顺序来跟踪 Hooks。这种方法也有其缺点,比如不能在条件语句中或循环中调用 Hooks。然而,这种方法使我们非常容易创建自定义 Hooks,并且简单易用易调试。我们也不需要担心命名钩子、名称冲突或编写包装函数。最终的 Hooks 方法让我们可以像使用任何其他函数一样使用 Hooks!

解决钩子的常见问题

正如我们发现的那样,使用官方 API 实现 Hooks 也有其自身的权衡和限制。我们现在将学习如何克服这些常见问题,这些问题源于 React Hooks 的限制。

我们将看看可以用来克服这两个问题的解决方案:

  • 解决条件钩子

  • 在循环中解决钩子

解决条件钩子

那么,如何实现条件 Hooks 呢?与其使 Hook 有条件,不如始终定义 Hook 并在需要时使用它。如果这不是一个选择,我们需要拆分我们的组件,这通常也更好!

始终定义 Hook

对于简单的情况,比如我们之前提到的名字示例,我们可以始终保持 Hook 的定义,如下:

const [ name, setName ] = useState('')

始终定义 Hook 通常是一个简单的解决方案。

拆分组件

解决条件 Hooks 的另一种方法是将一个组件拆分为多个组件,然后有条件地渲染这些组件。例如,假设我们想在用户登录后从数据库中获取用户信息。

我们不能这样做,因为使用if条件可能会改变 Hook 的顺序:

function UserInfo ({ username }) {
    if (username) {
        const info = useFetchUserInfo(username)
        return <div>{info}</div>
    }
    return <div>Not logged in</div>
}

相反,我们必须为用户登录时创建一个单独的组件,如下所示:

function LoggedInUserInfo ({ username }) {
    const info = useFetchUserInfo(username)
    return <div>{info}</div>
}

function UserInfo ({ username }) {
    if (username) {
        return <LoggedInUserInfo username={username} />
    }
    return <div>Not logged in</div>
}

为非登录和登录状态使用两个单独的组件总是有意义的,因为我们希望坚持一个组件一个功能的原则。因此,通常情况下,如果我们坚持最佳实践,不能使用条件 Hooks 并不是什么限制。

解决循环中的 Hooks

至于循环中的 Hooks,我们可以使用包含数组的单个 State Hook,或者我们可以拆分我们的组件。例如,假设我们想显示所有在线用户。

使用数组

我们可以简单地使用包含所有users的数组,如下所示:

function OnlineUsers ({ users }) {
    const [ userInfos, setUserInfos ] = useState([])
    // ... fetch & keep userInfos up to date ...
    return (
        <div>
            {users.map(username => {
                const user = userInfos.find(u => u.username === username)
                return <UserInfo {...user} />
            })}
        </div>
    )
}

然而,这可能并不总是有意义。例如,我们可能不希望通过OnlineUsers组件来更新user状态,因为我们需要从数组中选择正确的user状态,然后修改数组。这可能有效,但相当繁琐。

拆分组件

更好的解决方案是在UserInfo组件中使用 Hook。这样,我们可以保持每个用户的状态更新,而不必处理数组逻辑:

function OnlineUsers ({ users }) {
    return (
        <div>
            {users.map(username => <UserInfo username={username} />)}
        </div>
    )
}

function UserInfo ({ username }) {
    const info = useFetchUserInfo(username)
    // ... keep user info up to date ...
    return <div>{info}</div>
}

正如我们所看到的,为每个功能使用一个组件可以使我们的代码简单而简洁,并且避免了 React Hooks 的限制。

解决条件 Hooks 的问题

现在我们已经了解了条件 Hooks 的不同替代方案,我们将解决之前在我们的小示例项目中遇到的问题。这个问题的最简单解决方案是总是定义 Hook,而不是有条件地定义它。在这样一个简单的项目中,总是定义 Hook 是最合理的。

编辑src/App.js并删除以下条件 Hook:

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

用一个普通的 Hook 替换它,比如以下内容:

    const [ name, setName ] = useState('')

现在,我们的示例运行良好!在更复杂的情况下,总是定义 Hook 可能不可行。在这种情况下,我们需要创建一个新组件,在那里定义 Hook,然后有条件地渲染组件。

示例代码

简单解决条件 Hooks 问题的示例代码可以在Chapter02/chapter2_3文件夹中找到。

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

总结

在本章中,我们首先通过使用全局状态和闭包重新实现了useState函数。然后我们了解到,为了实现多个 Hooks,我们需要使用状态数组。然而,通过使用状态数组,我们被迫保持 Hooks 在函数调用中的顺序一致。这种限制使得条件 Hooks 和循环中的 Hooks 变得不可能。然后我们了解了 Hook API 的可能替代方案,它们的权衡以及为什么选择了最终的 API。最后,我们学会了如何解决由 Hooks 限制引起的常见问题。我们现在对 Hooks 的内部工作原理和限制有了扎实的理解。此外,我们深入了解了 State Hook。

在下一章中,我们将使用 State Hook 创建一个博客应用程序,并学习如何结合多个 Hooks。

问题

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

  1. 在开发我们自己的useState Hook 的重新实现时,我们遇到了什么问题?我们是如何解决这些问题的?

  2. 为什么在 React 的 Hooks 实现中条件 Hooks 不可能?

  3. Hooks 是什么,它们处理什么?

  4. 在使用 Hooks 时,我们需要注意什么?

  5. 替代 API 想法的常见问题是什么?

  6. 我们如何实现条件 Hooks?

  7. 我们如何在循环中实现 Hooks?

进一步阅读

如果您对本章学习的概念想了解更多,请参考以下阅读材料:

第三章:使用 React Hooks 编写您的第一个应用程序

深入了解 State Hook 后,我们现在将利用它从头开始创建一个博客应用程序。在本章中,我们将学习如何以可扩展的方式构建 React 应用程序,如何使用多个 Hooks,如何存储状态以及如何使用 Hooks 解决常见用例。在本章结束时,我们将拥有一个基本的博客应用程序,可以在其中登录、注册和创建帖子。

本章将涵盖以下主题:

  • 以可扩展的方式构建 React 项目

  • 从模拟中实现静态的 React 组件

  • 使用 Hooks 实现有状态的组件

技术要求

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

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

查看以下视频以查看代码的实际操作:

bit.ly/2Mm9yoC

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

现在,让我们开始本章。

构建 React 项目

在学习了 React 的原则、如何使用useState Hook 以及 Hooks 的内部工作原理后,我们现在将利用真正的useState Hook 来开发一个博客应用程序。首先,我们将创建一个新项目,并以一种可以在以后扩展项目的方式来构建文件夹结构。然后,我们将定义我们需要的组件,以涵盖博客应用程序的基本功能。最后,我们将使用 Hooks 为我们的应用程序引入状态!在本章中,我们还将学习JSX,以及在ES6ES2018中引入的新 JavaScript 功能。

文件夹结构

项目可以有许多不同的结构方式,不同的结构方式适用于不同的项目。通常,我们创建一个src/文件夹,并按功能将文件分组在那里。另一种流行的项目结构方式是按路由进行分组。对于一些项目,此外还可能根据代码的类型进行分离,比如src/api/src/components/。然而,对于我们的项目,我们主要关注用户界面UI)。因此,我们将按功能在src/文件夹中将文件分组。

最好一开始从一个简单的结构开始,只有在实际需要时才进行更深的嵌套。在开始项目时不要花太多时间考虑文件结构,因为通常情况下,你不知道文件应该如何分组。

选择功能

我们首先必须考虑在我们的博客应用程序中要实现哪些功能。至少,我们希望实现以下功能:

  • 注册用户

  • 登录/登出

  • 查看单个帖子

  • 创建新帖子

  • 列出帖子

既然我们已经选择了功能,让我们提出一个初始的文件夹结构。

提出一个初始结构

从我们之前的功能中,我们可以抽象出一些功能组:

  • 用户(注册,登录/登出)

  • 帖子(创建,查看,列出)

现在我们可以保持非常简单,将所有组件创建在src/文件夹中,不进行任何嵌套。然而,由于我们已经对博客应用程序需要的功能有了相当清晰的了解,我们现在可以提出一个简单的文件夹结构:

  • src/

  • src/user/

  • src/post/

在定义文件夹结构之后,我们可以继续进行组件结构。

组件结构

在 React 中,组件的理念是让每个组件处理单个任务或 UI 元素。我们应该尽量将组件做得细粒度,以便能够重用代码。如果我们发现自己在从一个组件复制和粘贴代码到另一个组件,那么创建一个新组件并在多个其他组件中重用它可能是个好主意。

通常,在开发软件时,我们会从 UI 模拟开始。对于我们的博客应用程序,模拟将如下所示:

我们博客应用程序的初始模拟

在拆分组件时,我们使用单一职责原则,该原则规定每个模块应对功能的一个封装部分负责。

在这个模拟中,我们可以在每个组件和子组件周围画框,并给它们命名。请记住,每个组件应该只负责一个功能。我们从构成这个应用程序的基本组件开始:

从我们的模拟中定义基本组件

我们为注销功能定义了一个Logout组件,一个包含创建新帖子表单的CreatePost组件,以及一个用于显示实际帖子的Post组件。

现在我们已经定义了我们的基本组件,我们将看看哪些组件在逻辑上属于一起,从而形成一个组。为此,我们现在定义容器组件,这样我们就可以将组件组合在一起:

从我们的模拟中定义容器组件

我们定义了一个PostList组件来将帖子分组,然后定义了一个UserBar组件来处理登录/注销和注册。最后,我们定义了一个App组件来将所有内容组合在一起,并定义我们应用程序的结构。

现在我们已经完成了对我们的 React 项目进行结构化,我们可以继续实现静态组件。

实现静态组件

在我们开始通过 Hooks 向我们的博客应用程序添加状态之前,我们将模拟应用程序的基本功能作为静态 React 组件。这样做意味着我们必须处理应用程序的静态视图结构。

首先处理静态结构是有意义的,这样可以避免以后将动态代码移动到不同的组件中。此外,首先只处理超文本标记语言(HTML)和 CSS 更容易——这有助于我们快速启动项目。然后,我们可以继续实现动态代码和处理状态。

逐步进行这一步,而不是一次实现所有内容,有助于我们快速启动新项目,而不必一次考虑太多,并且让我们避免以后重新构建项目!

设置项目

我们已经学会了如何设置一个新的 React 项目。正如我们所学到的,我们可以使用create-react-app工具轻松初始化一个新项目。我们现在要这样做:

  1. 首先,我们使用create-react-app来初始化我们的项目:
>npx create-react-app chapter3_1
  1. 然后,我们为我们的功能创建文件夹:
    • 创建文件夹src/user/
  • 创建文件夹src/post/

现在我们的项目结构已经设置好,我们可以开始实施组件。

实施用户

我们将从静态组件方面最简单的功能开始:实施与用户相关的功能。正如我们从模拟中看到的,我们在这里需要四个组件:

  • 一个Login组件,当用户尚未登录时我们将展示它

  • 一个Register组件,当用户尚未登录时我们也会展示它

  • 一个Logout组件,当用户登录后将显示

  • 一个UserBar组件,它将有条件地显示其他组件

我们将首先定义前三个组件,它们都是独立的组件。最后,我们将定义UserBar组件,因为它依赖于其他组件的定义。

登录组件

首先,我们定义Login组件,其中我们展示两个字段:用户名字段和密码字段。此外,我们展示一个登录按钮:

  1. 我们首先为我们的组件创建一个新文件:src/user/Login.js

  2. 在新创建的src/user/Login.js文件中,我们导入React

import  React  from  'react'
  1. 然后,我们定义我们的函数组件。目前,Login组件不会接受任何 props:
export  default  function  Login  ()  { 
  1. 最后,我们通过 JSX 返回两个字段和登录按钮。我们还定义了一个form容器元素来包裹它们。为了在提交表单时避免页面刷新,我们必须定义一个onSubmit处理程序并在事件对象上调用e.preventDefault()
    return (
        <form onSubmit={e => e.preventDefault()}>
            <label htmlFor="login-username">Username:</label>
            <input type="text" name="login-username" id="login-username" />
            <label htmlFor="login-password">Password:</label>
            <input type="password" name="login-password" id="login-password" />
            <input type="submit" value="Login" />
        </form>
    )
}

在这里,我们使用匿名函数来定义onSubmit处理程序。匿名函数的定义如下,如果它们没有任何参数:() => { ... },而不是function () { ... }。有了参数,我们可以写成(arg1, arg2) => { ... },而不是function (arg1, arg2) { ... }。如果我们只有一个参数,我们可以省略()括号。此外,如果我们的函数中只有一个语句,我们可以省略{}括号,就像这样:e => e.preventDefault()

使用语义化的 HTML 元素,如<form><label>,可以使您的应用程序更易于使用辅助功能软件的人导航,例如屏幕阅读器。此外,当使用语义化的 HTML 时,键盘快捷键,例如按回车键提交表单,会自动生效。

我们的Login组件已经实现,现在可以进行测试了。

测试我们的组件

既然我们已经定义了我们的第一个组件,让我们渲染它并看看它的样子:

  1. 首先,我们编辑src/App.js,并删除所有内容。

  2. 然后,我们首先导入ReactLogin组件:

import React from 'react'

import Login from './user/Login'

将导入分组成属于一起的代码块是一个好主意。在这种情况下,我们通过在外部导入(如 React)和本地导入(如我们的Login组件)之间添加空行来分隔它们。这样做可以保持我们的代码可读性,特别是当我们以后添加更多导入语句时。

  1. 最后,我们定义App组件,并返回Login组件:
export default function App () {
    return <Login />
}

如果我们只返回一个组件,可以在return语句中省略括号。而不是写return (<Login />),我们可以简单地写return <Login />

  1. 在浏览器中打开http://localhost:3000,您应该看到Login组件被渲染。如果您已经在浏览器中打开了页面,当您更改代码时,它应该会自动刷新:

我们博客应用的第一个组件:通过用户名和密码登录

正如我们所看到的,静态的Login组件在 React 中渲染得很好。现在我们可以继续进行Logout组件。

登出组件

接下来,我们定义Logout组件,它将显示当前登录的用户和一个登出按钮:

  1. 创建一个新文件:src/user/Logout.js

  2. 导入React,如下所示:

import React from 'react'
  1. 这次,我们的函数将接受一个user属性,我们将使用它来显示当前登录的用户:
export default function Logout ({ user }) {

在这里,我们使用解构来从props对象中提取user键。React 将所有组件 props 作为单个对象作为函数的第一个参数传递。在第一个参数上使用解构类似于在类组件中执行const { user } = this.props

  1. 最后,我们返回一个文本,显示当前登录的user和登出按钮:
    return (
        <form onSubmit={e => e.preventDefault()}>
            Logged in as: <b>{user}</b>
            <input type="submit" value="Logout" />
        </form>
    )
}
  1. 现在,我们可以在src/App.js中用Logout组件替换Login组件,以便看到我们新定义的组件(不要忘记将user属性传递给它!):
import React from 'react'

import Logout from './user/Logout'

export default function App () {
    return <Logout user="Daniel Bugl" />
}

现在Logout组件已经定义,我们可以继续定义Register组件。

注册组件

静态的Register组件将与Login组件非常相似,只是多了一个重复密码的字段。如果它们如此相似,您可能会想将它们合并为一个组件,并添加一个 prop 来切换重复密码字段。然而,最好遵循单一职责原则,让每个组件只处理一个功能。稍后,我们将使用动态代码扩展静态组件,然后RegisterLogin的代码将大不相同。因此,我们稍后需要再次拆分它们。

尽管如此,让我们开始编写Register组件的代码:

  1. 首先,我们创建一个新的src/user/Register.js文件,并从Login组件中复制代码,因为静态组件毕竟非常相似。确保将组件的名称更改为Register
import React from 'react'

export default function Register () {
    return (
        <form onSubmit={e => e.preventDefault()}>
            <label htmlFor="register-username">Username:</label>
            <input type="text" name="register-username" id="register-username" />
            <label htmlFor="register-password">Password:</label>
            <input type="password" name="register-password" id="register-password" />
  1. 接下来,我们在 Password 字段代码下方添加重复密码字段:
            <label htmlFor="register-password-repeat">Repeat password:</label>
            <input type="password" name="register-password-repeat" id="register-password-repeat" />
  1. 最后,我们还将提交按钮的值更改为 Register:
            <input type="submit" value="Register" />
        </form>
    )
}
  1. 同样,我们可以编辑src/App.js以类似的方式显示我们的组件,就像我们在Login组件中所做的那样:
import React from 'react'

import Register from './user/Register'

export default function App () {
    return <Register />
}

正如我们所看到的,我们的Register组件看起来与Login组件非常相似。

UserBar 组件

现在是时候将我们与用户相关的组件放在一个UserBar组件中了。在这里,我们将有条件地显示LoginRegister组件,或者Logout组件,这取决于用户是否已经登录。

让我们开始实现UserBar组件:

  1. 首先,我们创建一个新的src/user/UserBar.js文件,并导入React以及我们定义的三个组件:
import React from 'react'

import Login from './Login'
import Logout from './Logout'
import Register from './Register'
  1. 接下来,我们定义我们的函数组件,并为user定义一个值。现在,我们只是将它保存在一个静态变量中:
export default function UserBar () {
    const user = ''
  1. 然后,我们检查用户是否已登录。如果用户已登录,我们显示Logout组件,并将user值传递给它:
    if (user) {
        return <Logout user={user} />
  1. 否则,我们展示LoginRegister组件。在这里,我们可以使用React.Fragment而不是<div>容器元素。这样可以保持我们的 UI 树干净,因为组件将简单地并排渲染,而不是包裹在另一个元素中:
    } else {
        return (
            <React.Fragment>
                <Login />
                <Register />
            </React.Fragment>
        )
    }
}
  1. 再次编辑src/App.js,现在我们展示我们的UserBar组件:
import React from 'react'

import UserBar from './user/UserBar'

export default function App () {
    return <UserBar />
}
  1. 我们可以看到,它起作用了!我们现在展示LoginRegister组件:

我们的 UserBar 组件,展示了 Login 和 Register 组件

  1. 接下来,我们可以编辑src/user/UserBar.js文件,并将user值设置为一个字符串:
        const user = 'Daniel Bugl' 
  1. 这样做之后,我们的应用程序现在显示Logout组件:

我们的应用程序在定义user值后显示了 Logout 组件

在本章的后面,我们将向我们的应用程序添加 Hooks,这样我们就可以登录并使状态动态更改,而无需编辑代码!

示例代码

与用户相关的组件的示例代码可以在Chapter03/chapter3_1文件夹中找到。

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

实现帖子

在实现了所有与用户相关的组件之后,我们继续在博客应用中实现帖子。我们将定义以下组件:

  • 一个Post组件用于显示单个帖子

  • 一个CreatePost组件用于创建新的帖子

  • 一个PostList组件用于显示多个帖子

现在让我们开始实现与帖子相关的组件。

Post 组件

在创建模型时,我们已经考虑了帖子具有哪些元素。帖子应该有一个标题,内容和作者(撰写帖子的用户)。

现在让我们实现Post组件:

  1. 首先,我们创建一个新文件:src/post/Post.js

  2. 然后,我们导入React,并定义我们的函数组件,接受三个属性:titlecontentauthor

import React from 'react'

export default function Post ({ title, content, author }) {
  1. 接下来,我们以类似模型的方式呈现所有属性:
    return (
        <div>
            <h3>{title}</h3>
            <div>{content}</div>
            <br />
            <i>Written by <b>{author}</b></i>
        </div>
    )
}
  1. 像往常一样,我们可以通过编辑src/App.js文件来测试我们的组件:
import React from 'react'

import Post from './post/Post'

export default function App () {
    return <Post title="React Hooks" content="The greatest thing since sliced bread!" author="Daniel Bugl" />
}

现在,静态的Post组件已经实现,我们可以继续进行CreatePost组件。

CreatePost 组件

接下来,我们实现一个表单来允许创建新的帖子。在这里,我们将user值作为属性传递给组件,因为作者应该始终是当前登录的用户。然后,我们显示作者,并为博客帖子的title提供一个输入字段,以及一个<textarea>元素用于内容。

现在让我们实现CreatePost组件:

  1. 创建一个新文件:src/post/CreatePost.js

  2. 定义以下组件:

import React from 'react'

export default function CreatePost ({ user }) {
    return (
        <form onSubmit={e => e.preventDefault()}>
            <div>Author: <b>{user}</b></div>
            <div>
                <label htmlFor="create-title">Title:</label>
                <input type="text" name="create-title" id="create-title" />
            </div>
            <textarea />
            <input type="submit" value="Create" />
        </form>
    )
}
  1. 像往常一样,我们可以通过编辑src/App.js文件来测试我们的组件:
import React from 'react'

import CreatePost from './post/CreatePost'

export default function App () {
    return <CreatePost />
}

正如我们所看到的,CreatePost组件渲染正常。我们现在可以继续进行PostList组件。

PostList 组件

在实现其他与文章相关的组件之后,我们现在可以实现博客应用程序最重要的部分:博客文章的动态更新。目前,动态更新只是简单地显示博客文章列表。

让我们现在开始实现PostList组件:

  1. 我们首先导入ReactPost组件:
import React from 'react'

import Post from './Post'
  1. 然后,我们定义我们的PostList函数组件,接受一个posts数组作为 prop。如果posts未定义,我们将其默认设置为空数组:
export default function PostList ({ posts = [] }) {
  1. 接下来,我们使用.map函数和扩展语法来渲染所有posts
    return (
        <div>
            {posts.map((p, i) => <Post {...p} key={'post-' + i} />)}
        </div>
    )
}

如果我们要渲染一个元素列表,我们必须给每个元素一个唯一的key prop。当数据发生变化时,React 使用这个key prop 来高效地计算两个列表的差异。

在这里,我们使用map函数,它将一个函数应用于数组的所有元素。这类似于使用for循环并存储所有结果,但它更加简洁、声明性,并且更容易阅读!或者,我们可以使用map函数的替代方法:

let renderedPosts = []
let i = 0
for (let p of posts) {
    renderedPosts.push(<Post {...p} key={'post-' + i} />)
    i++
}

return (
    <div>
        {renderedPosts}
    </div>
)

然后我们为每篇文章返回<Post>组件,并将文章对象p的所有键作为 props 传递给组件。我们使用扩展语法来实现这一点,它的效果与手动列出对象中所有键作为 props 相同,如下所示:<Post title={p.title} content={p.content} author={p.author} />

  1. 在模型中,每篇博客文章之后都有一条水平线。我们可以通过使用React.Fragment来实现这一点,而无需额外的<div>容器元素:
{posts.map((p, i) => (
     <React.Fragment key={'post-' + i} >
          <Post {...p} />
          <hr />
     </React.Fragment>
))}

key prop 始终必须添加到在map函数中渲染的最上层父元素。在这种情况下,我们不得不将key prop 从Post组件移动到React.Fragment组件中。

  1. 我们通过编辑src/App.js文件来测试我们的组件:
import React from 'react'

import PostList from './post/PostList'

const posts = [
 { title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' },
 { title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl' }
]

export default function App () {
    return <PostList posts={posts} />
}

现在,我们可以看到我们的应用程序列出了我们在posts数组中定义的所有文章:

使用 PostList 组件显示多篇文章

正如我们所看到的,通过PostList组件列出多篇文章是可以的。现在我们可以继续组合应用程序。

组合应用程序

在实现所有组件之后,为了复制模型,我们现在只需要将所有内容放在App组件中。然后,我们将成功复制模型!

让我们开始修改App组件,并组合我们的应用程序:

  1. 编辑src/App.js,并删除所有当前代码。

  2. 首先,我们导入ReactPostListCreatePostUserBar组件:

import React from 'react'

import PostList from './post/PostList'
import CreatePost from './post/CreatePost'
import UserBar from './user/UserBar'
  1. 然后,我们为我们的应用程序定义一些模拟数据:
const user = 'Daniel Bugl'
const posts = [
    { title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' },
    { title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl' }
]
  1. 接下来,我们定义App组件,并返回一个<div>容器元素,在这里我们设置一些填充:
export default function App () {
    return (
        <div style={{ padding: 8 }}>
  1. 现在,我们插入UserBarCreatePost组件,将user属性传递给CreatePost组件:
            <UserBar />
            <br />
            <CreatePost user={user} />
            <br />
            <hr />

请注意,您应该始终优先使用 CSS 进行间距设置,而不是使用<br />HTML 标记。但是,目前我们专注于 UI,而不是其样式,因此我们尽可能使用 HTML。

  1. 最后,我们显示PostList组件,列出所有的posts
            <PostList posts={posts} />
        </div>
    )
}
  1. 保存文件后,http://localhost:3000应该会自动刷新,现在我们可以看到完整的 UI 了:

根据模拟的静态博客应用程序的完整实现

正如我们所看到的,我们之前定义的所有静态组件都在一个App组件中一起呈现。我们的应用程序现在看起来就像模拟一样。接下来,我们可以继续使所有组件都变得动态。

示例代码

我们博客应用程序静态实现的示例代码可以在Chapter03/chapter3_2文件夹中找到。

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

使用 Hooks 实现有状态的组件

现在我们已经实现了应用程序的静态结构,我们将为它添加useState Hooks,以便能够处理状态和动态交互!

为用户功能添加 Hooks

为了为用户功能添加 Hooks,我们需要用一个 State Hook 替换静态的user值。然后,我们需要在登录、注册和注销时调整这个值。

调整 UserBar

回想一下,当我们创建UserBar组件时,我们静态定义了user值。现在我们将用一个 State Hook 替换这个值!

让我们开始修改UserBar组件,使其变得动态:

  1. 编辑src/user/UserBar.js,通过调整React导入语句导入useState Hook,如下所示:
import React, { useState } from 'react'
  1. 删除以下代码行:
    const user = 'Daniel Bugl'

用一个空的用户''作为默认值替换它:

    const [ user, setUser ] = useState('')
  1. 然后,我们将setUser函数传递给LoginRegisterLogout组件:
    if (user) {
        return <Logout user={user} setUser={setUser} />
    } else {
        return (
            <React.Fragment>
                <Login setUser={setUser} />
                <Register setUser={setUser} />
            </React.Fragment>
        )
    }

现在,UserBar组件提供了一个setUser函数,可以在LoginRegisterLogout组件中使用,以设置或取消user的值。

调整登录和注册组件

LoginRegister组件中,我们需要使用setUser函数来相应地设置user的值,当我们登录或注册时。

登录

Login组件中,我们现在暂时忽略密码字段,只处理用户名字段。

让我们首先修改Login组件以使其动态化:

  1. 编辑src/user/Login.js,并导入useState Hook:
import React, { useState } from 'react'
  1. 然后,调整函数定义以接受setUser属性:
export default function Login ({ setUser }) {
  1. 现在,我们为用户名字段的值定义一个新的 State Hook:
    const [ username, setUsername ] = useState('')
  1. 接下来,我们定义一个处理程序函数:
    function handleUsername (evt) {
        setUsername(evt.target.value)
    }
  1. 然后,我们调整input字段,以使用username的值,并在输入更改时调用handleUsername函数:
            <input type="text" value={username} onChange={handleUsername} name="login-username" id="login-username" />
  1. 最后,当按下登录按钮并且表单被提交时,我们需要调用setUser函数:
            <form onSubmit={e => { e.preventDefault(); setUser(username) }} />
  1. 此外,当username值为空时,我们可以禁用登录按钮:
            <input type="submit" value="Login" disabled={username.length === 0} />

它起作用了——我们现在可以输入用户名,按下登录按钮,然后我们的UserBar组件将改变其状态,并显示Logout组件!

注册

对于注册,我们还将检查输入的密码是否相同,只有在这种情况下我们才会设置user的值。

让我们首先修改Register组件以使其动态化:

  1. 首先,我们执行与“登录”相同的步骤,以处理“用户名”字段:
import React, { useState } from 'react'

export default function Register ({ setUser }) {
 const [ username, setUsername ] = useState('')

 function handleUsername (evt) {
 setUsername(evt.target.value)
 }

    return (
        <form onSubmit={e => { e.preventDefault(); setUser(username) }}>
            <label htmlFor="register-username">Username:</label>
            <input type="text" value={username} onChange={handleUsername} name="register-username" id="register-username" />
            <label htmlFor="register-password">Password:</label>
            <input type="password" name="register-password" id="register-password" />
            <label htmlFor="register-password-repeat">Repeat password:</label>
            <input type="password" name="register-password-repeat" id="register-password-repeat" />
            <input type="submit" value="Register" disabled={username.length === 0} />
        </form>
    )
}
  1. 现在,我们为密码重复密码字段定义了两个新的 State Hooks:
    const [ password, setPassword ] = useState('')
    const [ passwordRepeat, setPasswordRepeat ] = useState('')
  1. 然后,我们为它们定义两个处理程序函数:
    function handlePassword (evt) {
        setPassword(evt.target.value)
    }

    function handlePasswordRepeat (evt) {
        setPasswordRepeat(evt.target.value)
    }

您可能已经注意到,我们总是为input字段编写类似的处理程序函数。实际上,这是创建自定义 Hook 的完美用例!我们将在未来的章节中学习如何做到这一点。

  1. 接下来,我们将valueonChange处理程序函数分配给input字段:
             <label htmlFor="register-password">Password:</label>
             <input type="password" value={password} onChange={handlePassword} name="register-password" id="register-password" />
             <label htmlFor="register-password-repeat">Repeat password:</label>
             <input type="password" value={passwordRepeat} onChange={handlePasswordRepeat} name="register-password-repeat" id="register-password-repeat" />
  1. 最后,我们检查密码是否匹配,如果不匹配,我们保持按钮处于禁用状态:
             <input type="submit" value="Register" disabled={username.length === 0 || password.length === 0 || password !== passwordRepeat} />

现在我们成功地实现了检查密码是否相等,并且我们实现了注册!

调整登出

对于用户功能,还有一件事情还缺少——我们还不能注销。

现在让我们使Logout组件动态化:

  1. 编辑src/user/Logout.js,并添加setUser属性:
export default function Logout ({ user, setUser }) {
  1. 然后,调整formonSubmit处理程序并将用户设置为''
            <form onSubmit={e => { e.preventDefault(); setUser('') }} />

由于我们在这里不创建新的 Hook,所以不需要从 React 中导入useState Hook。我们可以简单地使用传递给Logout组件的setUser函数作为 prop。

现在,当我们点击注销按钮时,Logout组件将user值设置为''

将用户传递给 CreatePost

你可能已经注意到,CreatePost组件仍然使用硬编码的用户名。为了能够在那里访问user值,我们需要将 Hook 从UserBar组件移动到App组件。

现在让我们重构user State Hook 的定义:

  1. 编辑src/user/UserBar.js,并删除那里的 Hook 定义:
    const [ user, setUser ] = useState('')
  1. 然后,我们编辑函数定义,并接受这两个值作为 props:
export default function UserBar ({ user, setUser }) {
  1. 现在,我们编辑src/App.js,并在那里导入useState Hook:
import React, { useState } from 'react'
  1. 接下来,我们删除静态的user值定义:
    const user = 'Daniel Bugl'
  1. 然后,我们将之前剪切的user State Hook 插入App组件函数中:
    const [ user, setUser ] = useState('')
  1. 现在,我们可以将usersetUser作为 props 传递给UserBar组件:
            <UserBar user={user} setUser={setUser} />

user状态是全局状态,因此我们需要在应用程序中的许多组件中使用它。目前,这意味着我们需要将user值和setUser函数传递给每个需要它的组件。在未来的章节中,我们将学习关于 React Context Hooks,它解决了必须以这种方式传递 props 的问题。

  1. 最后,只有在用户登录时才显示CreatePost组件。为了做到这一点,我们使用一种模式,它允许我们根据条件显示组件:
 {user && <CreatePost user={user} />}

现在,用户功能已经完全实现了——我们可以使用LoginRegister组件,并且user值也传递给了CreatePost组件!

为帖子功能添加 Hooks

实现用户功能后,我们现在要实现动态创建帖子。我们首先调整App组件,然后修改CreatePost组件,以便能够插入新帖子。

让我们开始调整 App 组件。

调整 App 组件

正如我们从用户功能中所知道的,帖子也将是全局状态,因此我们应该在App组件中定义它。

现在让我们将posts值作为全局状态实现:

  1. 编辑src/App.js,并将当前的posts数组重命名为defaultPosts
const defaultPosts = [
    { title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' },
    { title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl' }
]
  1. 然后,为posts状态定义一个新的 State Hook:
    const [ posts, setPosts ] = useState(defaultPosts)
  1. 现在,我们将posts值和setPosts函数作为 props 传递给CreatePost组件:
            {user && <CreatePost user={user} posts={posts} setPosts={setPosts} />}

现在,我们的App组件为CreatePost组件提供了posts数组和setPosts函数。让我们继续调整 CreatePost 组件。

调整 CreatePost 组件

接下来,我们需要使用setPosts函数来在按下 Create 按钮时插入一个新的帖子。

让我们开始修改CreatePost组件,以使其动态化:

  1. 编辑src/posts/CreatePost.js,并导入useState Hook:
import React, { useState } from 'react'
  1. 然后,调整函数定义以接受postssetPosts属性:
export default function CreatePost ({ user, posts, setPosts }) {
  1. 接下来,我们定义两个新的 State Hooks——一个用于title值,一个用于content值:
    const [ title, setTitle ] = useState('')
    const [ content, setContent ] = useState('')
  1. 现在,我们定义了两个处理函数——一个用于input字段,一个用于textarea
    function handleTitle (evt) {
        setTitle(evt.target.value)
    }

    function handleContent (evt) {
        setContent(evt.target.value)
    }
  1. 我们还为 Create 按钮定义了一个处理函数:
    function handleCreate () {
  1. 在这个函数中,我们首先从input字段的值创建一个newPost对象:
        const newPost = { title, content, author: user }

在较新的 JavaScript 版本中,我们可以将以下对象赋值缩短为{ title: title },变为{ title },并且会产生相同的效果。因此,我们可以简单地使用{ title, contents }来代替{ title: title, contents: contents }

  1. 然后,我们通过首先将newPost添加到数组中,然后使用扩展语法列出所有现有的posts来设置新的posts数组:
        setPosts([ newPost, ...posts ])
    }
  1. 接下来,我们将value和处理函数添加到input字段和textarea元素中:
             <div>
                 <label htmlFor="create-title">Title:</label>
                 <input type="text" value={title} onChange={handleTitle} name="create-title" 
                        id="create-title" />
             </div>
             <textarea value={content} onChange={handleContent} />

通常在 HTML 中,我们将textarea的值放在其子元素中。然而,在 React 中,textarea可以像任何其他input字段一样处理,通过使用valueonChange属性。

  1. 最后,我们将handleCreate函数传递给form元素的onSubmit处理程序:
         <form onSubmit={e => { e.preventDefault(); handleCreate() }}>
  1. 现在,我们可以登录并创建一个新的帖子,它将被插入到动态源的开头:

使用 Hooks 插入新博客帖子后的博客应用程序的第一个版本

正如我们所看到的,现在我们的应用程序是完全动态的,我们可以使用它的所有功能!

示例代码

使用 Hooks 实现我们的博客应用程序的动态示例代码可以在Chapter03/chapter3_3文件夹中找到。

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

总结

在本章中,我们从头开始开发了自己的博客应用程序!我们从一个模型开始,然后创建了静态组件来模拟它。之后,我们实现了 Hooks,以实现动态行为。在整个章节中,我们学会了如何使用 Hooks 处理本地和全局状态。此外,我们学会了如何使用多个 Hooks,以及在哪些组件中定义 Hooks 和存储状态。我们还学会了如何解决常见用例,比如使用 Hooks 处理输入字段。

在下一章中,我们将学习useReducer Hook,它使我们能够更轻松地处理特定状态变化。此外,我们将学习useEffect Hook,它使我们能够运行具有副作用的代码。

问题

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

  1. 在 React 中,文件夹结构的最佳实践是什么?

  2. 在拆分 React 组件时应该使用哪个原则?

  3. map函数是做什么的?

  4. 解构是如何工作的,我们什么时候使用它?

  5. 展开运算符是如何工作的,我们什么时候使用它?

  6. 我们如何使用 React Hooks 处理输入字段?

  7. 本地状态 Hook 应该在哪里定义?

  8. 什么是全局状态?

  9. 全局状态 Hook 应该在哪里定义?

进一步阅读

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

第二部分:深入理解 Hooks

在本书的这一部分,我们将学习各种 React Hooks 以及如何使用它们。此外,我们还将学习 Hooks 的规则以及如何编写我们自己的 Hooks。

在本节中,我们将涵盖以下章节:

  • 第四章,使用 Reducer 和 Effect Hooks

  • 第五章,实现 React Context

  • 第六章,实现请求和 React Suspense

  • 第七章,使用 Hooks 进行路由

  • 第八章,使用社区提供的 Hooks

  • 第九章,Hooks 的规则

  • 第十章,构建自己的 Hooks

第四章:在我们的博客应用中使用 Reducer 和 Effect Hooks

在使用 State Hook 开发我们自己的博客应用之后,我们现在要学习 React 提供的另外两个非常重要的 Hooks:ReducerEffect Hooks。我们首先要学习何时应该使用 Reducer Hook 而不是 State Hook。然后,我们学习如何将现有的 State Hook 转换为 Reducer Hook,以便在实践中掌握这个概念。接下来,我们将学习 Effect Hooks 以及它们的用途。最后,我们将在我们的博客应用中实现它们。

本章将涵盖以下主题:

  • 学习有关 Reducer Hooks 和 State Hooks 之间的区别

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

  • 在我们的博客应用中使用 Effect Hooks

技术要求

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

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

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

bit.ly/2Mm9yoC

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

现在,让我们开始本章。

Reducer Hooks 与 State Hooks

在上一章中,我们学习了如何处理本地和全局状态。我们对两种情况都使用了 State Hooks,这对于简单的状态更改是可以的。然而,当我们的状态逻辑变得更加复杂时,我们需要确保保持状态一致。为了做到这一点,我们应该使用 Reducer Hook 而不是多个 State Hooks,因为很难在彼此依赖的多个 State Hooks 之间保持同步。作为替代方案,我们可以将所有状态保存在一个 State Hook 中,但然后我们必须确保不会意外地覆盖我们状态的部分。

State Hook 的问题

State Hook 已经支持向其传递复杂对象和数组,并且可以很好地处理它们的状态变化。然而,我们总是需要直接改变状态,这意味着我们需要使用大量的扩展语法,以确保我们不会覆盖状态的其他部分。例如,想象一下,我们有一个这样的状态对象:

const [ config, setConfig ] = useState({ filter: 'all', expandPosts: true })

现在,我们想要改变过滤器:

setConfig({ filter: { byAuthor: 'Daniel Bugl', fromDate: '2019-04-29' } })

如果我们简单地运行前面的代码,我们将删除状态的expandPosts部分!所以,我们需要做以下操作:

setConfig({ ...config, filter: { byAuthor: 'Daniel Bugl', fromDate: '2019-04-29' } })

现在,如果我们想要将fromDate过滤器更改为不同的日期,我们需要两次使用扩展语法,以避免删除byAuthor过滤器:

setConfig({ ...config, filter: { ...config.filter, fromDate: '2019-04-30' } })

但是,如果我们在filter状态仍然是字符串时这样做会发生什么?我们将得到以下结果:

{ filter: { '0': 'a', '1': 'l', '2': 'l', fromDate: '2019-04-30' },
  expandPosts: true }

什么?为什么突然出现了三个新键—012?这是因为扩展语法也适用于字符串,它们以这样的方式扩展,即每个字母根据其在字符串中的索引获得一个键。

正如你所想象的那样,对于较大的状态对象,使用扩展语法和直接改变状态对象会变得非常繁琐。此外,我们总是需要确保我们不会引入任何错误,并且需要在应用程序的各个地方检查错误。

操作

而不是直接改变状态,我们可以创建一个处理状态变化的函数。这样的函数只允许通过特定操作来改变状态,比如CHANGE_FILTERTOGGLE_EXPAND操作。

操作只是具有type键的对象,告诉我们我们正在处理哪个操作,并且更详细地描述操作的其他键。

TOGGLE_EXPAND操作非常简单。它只是一个定义了type的对象:

{ type: 'TOGGLE_EXPAND' }

CHANGE_FILTER操作可以处理我们之前遇到的复杂状态变化问题,如下所示:

{ type: 'CHANGE_FILTER', all: true }
{ type: 'CHANGE_FILTER', fromDate: '2019-04-29' }
{ type: 'CHANGE_FILTER', byAuthor: 'Daniel Bugl' }
{ type: 'CHANGE_FILTER', fromDate: '2019-04-30' }

第二、第三和第四个操作将把filter状态从字符串更改为对象,然后设置相应的键。如果对象已经存在,我们只需调整在操作中定义的键。每个操作后,状态将如下更改:

  • { expandPosts: true, filter: 'all' }

  • { expandPosts: true, filter: **{** fromDate: '2019-04-29' **}** }

  • { expandPosts: true, filter: { fromDate: '2019-04-29', byAuthor: 'Daniel Bugl' } }

  • { expandPosts: true, filter: { fromDate: '2019-04-30', byAuthor: 'Daniel Bugl' } }

现在,看一下以下代码:

{ type: 'CHANGE_FILTER', all: true }

如果我们分派了另一个 action,就像前面的代码一样,那么状态将回到初始状态中的all字符串。

Reducers

现在,我们仍然需要定义处理这些状态变化的函数。这样的函数被称为 reducer 函数。它以当前的stateaction作为参数,并返回一个新的 state。

如果您熟悉 Redux 库,您可能已经非常熟悉状态、actions 和 reducers 的概念。

现在,我们要定义我们的reducer函数:

  1. 我们从我们的reducer的函数定义开始:
function reducer (state, action) {
  1. 然后,我们使用switch语句检查action.type
    switch (action.type) {
  1. 现在,我们要处理TOGGLE_EXPAND动作,我们只是切换当前的expandPosts状态:
        case 'TOGGLE_EXPAND':
            return { ...state, expandPosts: !state.expandPosts }
  1. 接下来,我们要处理CHANGE_FILTER动作。在这里,我们首先需要检查all是否设置为true,在这种情况下,只需将我们的filter设置为'all'字符串:
        case 'CHANGE_FILTER':
            if (action.all) {
                return { ...state, filter: 'all' }
            }
  1. 现在,我们必须处理其他filter选项。首先,我们检查filter变量是否已经是一个object。如果不是,我们创建一个新的。否则,我们使用现有的对象:
            let filter = typeof state.filter === 'object' ? state.filter : {}
  1. 然后,我们定义各种过滤器的处理程序,允许同时设置多个过滤器,而不是立即返回新的state
            if (action.fromDate) {
                filter = { ...filter, fromDate: action.fromDate }
            }
            if (action.byAuthor) {
                filter = { ...filter, byAuthor: action.byAuthor }
            }
  1. 最后,我们返回新的state
            return { ...state, filter }
  1. 对于default情况,我们抛出错误,因为这是一个未知的动作:
        default:
            throw new Error()
    }
}

在默认情况下抛出错误与 Redux reducers 的最佳实践不同,在那里我们只会在默认情况下返回当前状态。因为 React Reducer Hooks 不会将所有状态存储在一个对象中,我们只会处理特定状态对象的某些动作,所以我们可以对未知的动作抛出错误。

现在,我们的reducer函数已经定义,我们可以继续定义 Reducer Hook。

Reducer Hook

现在我们已经定义了 actions 和reducer函数,我们可以从reducer创建一个 Reducer Hook。useReducer Hook 的签名如下:

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

我们唯一还需要定义的是initialState;然后我们可以定义一个 Reducer Hook:

const initialState = { all: true }

现在,我们可以通过使用从 Reducer Hook 返回的state对象来访问状态,并通过dispatch函数分派 actions,如下所示:

dispatch({ type: 'TOGGLE_EXPAND' })

如果我们想要向 action 添加其他选项,我们只需将它们添加到 action 对象中:

dispatch({ type: 'CHANGE_FILTER', fromDate: '2019-04-30' })

正如我们所看到的,使用操作和减速器处理状态变化比直接调整状态对象要容易得多。

实现 Reducer Hooks

在了解了操作、减速器和 Reducer Hook 之后,我们将在我们的博客应用程序中实现它们。当状态对象或状态变化变得太复杂时,任何现有的 State Hook 都可以转换为 Reducer Hook。

如果有多个setState函数总是同时调用,这是它们应该在一个单独的 Reducer Hook 中分组的一个很好的提示。

全局状态通常是使用 Reducer Hook 的一个很好的候选,而不是 State Hook,因为全局状态的变化可以发生在应用程序的任何地方。然后,在一个地方处理操作并仅更新状态变化逻辑会更容易。将所有状态变化逻辑放在一个地方使得更容易维护和修复错误,而不会因忘记在所有地方更新逻辑而引入新错误。

现在,我们将一些现有的 State Hooks 在我们的博客应用程序中转换为 Reducer Hooks。

将 State Hook 转换为 Reducer Hook

在我们的博客应用程序中,我们有两个全局 State Hooks,我们将用 Reducer Hooks 替换它们:

  • user状态

  • posts状态

我们首先替换user State Hook。

替换用户 State Hook

我们将从user State Hook 开始,因为它比posts State Hook 更简单。以后,user状态将包含复杂的状态变化,因此在这里使用 Reducer Hook 是有意义的。

首先,我们将定义我们的操作,然后我们将定义减速器函数。最后,我们将用 Reducer Hook 替换 State Hook。

定义操作

我们首先定义我们的操作,因为在定义减速器函数时,这些操作将很重要。

现在让我们定义操作:

  1. 首先,我们需要一个操作来允许用户通过提供username值和password值来登录:
{ type: 'LOGIN', username: 'Daniel Bugl', password: 'notsosecure' }
  1. 然后,我们还需要一个REGISTER操作,在我们的情况下,它将类似于LOGIN操作,因为我们还没有实现任何注册逻辑:
{ type: 'REGISTER', username: 'Daniel Bugl', password: 'notsosecure', passwordRepeat: 'notsosecure' }
  1. 最后,我们将需要一个LOGOUT操作,它只是简单地注销当前登录的用户:
{ type: 'LOGOUT' }

现在,我们已经定义了所有必需的与用户相关的操作,我们可以继续定义减速器函数了。

定义减速器

接下来,我们为user状态定义一个减速器函数。现在,我们将把我们的减速器放在src/App.js文件中。

以后,创建一个单独的src/reducers.js文件,甚至是一个单独的src/reducers/目录,为每个 reducer 函数创建单独的文件可能是有意义的。

让我们开始定义userReducer函数:

  1. src/App.js文件中,在App函数定义之前,为user状态创建一个userReducer函数:
function userReducer (state, action) {
  1. 再次,我们对动作类型使用switch语句:
    switch (action.type) {
  1. 然后,我们处理LOGINREGISTER动作,将user状态设置为给定的username值。在我们的情况下,我们暂时只是从action对象中返回username值:
        case 'LOGIN':
        case 'REGISTER':
            return action.username
  1. 接下来,我们处理LOGOUT动作,将状态设置为空字符串:
        case 'LOGOUT':
            return ''
  1. 最后,当遇到未处理的动作时,我们会抛出一个错误:
        default:
            throw new Error()
    }
}

现在,userReducer函数已经定义,我们可以继续定义 Reducer Hook。

定义 Reducer Hook

定义完动作和 reducer 函数后,我们将定义 Reducer Hook,并将其状态和 dispatch 函数传递给需要它的组件。

让我们开始实现 Reducer Hook:

  1. 首先,我们需要通过调整src/App.js中的以下import语句来导入useReducer Hook:
import React, { useState, useReducer } from 'react'
  1. 编辑src/App.js,移除以下 State Hook:
    const [ user, setUser ] = useState('')

用 Reducer Hook 替换前面的 State Hook——初始状态是一个空字符串,就像以前一样:

    const [ user, dispatchUser ] = useReducer(userReducer, '')
  1. 现在,将user状态和dispatchUser函数作为dispatch属性传递给UserBar组件:
            <UserBar user={user} dispatch={dispatchUser} />
  1. 我们不需要修改CreatePost组件,因为我们只是将user状态传递给它,而这部分没有改变。

  2. 接下来,我们编辑src/user/UserBar.js中的UserBar组件,并用dispatch函数替换setUser属性:

export default function UserBar ({ user, dispatch }) {
    if (user) {
        return <Logout user={user} dispatch={dispatch} />
    } else {
        return (
            <React.Fragment>
                <Login dispatch={dispatch} />
                <Register dispatch={dispatch} />
            </React.Fragment>
        )
    }
}
  1. 现在,我们可以编辑src/user/Login.js中的Login组件,并用dispatch函数替换setUser函数:
export default function Login ({ dispatch }) {
  1. 然后,我们用dispatch函数替换了对setUser的调用,派发一个LOGIN动作:
            <form onSubmit={e => { e.preventDefault(); dispatch({ type: 'LOGIN', username }) }}>

我们还可以创建返回动作的函数,即所谓的动作创建者。我们可以简单地调用loginAction('username'),而不是每次手动创建动作对象,它会返回相应的LOGIN动作对象。

  1. 我们在src/user/Register.js中的Register组件中重复相同的过程:
export default function Register ({ dispatch }) {
    // ...
            <form onSubmit={e => { e.preventDefault(); dispatch({ type: 'REGISTER', username }) }}>
  1. 最后,我们也在src/user/Logout.js中的Logout组件中重复相同的过程:
export default function Logout ({ user, dispatch }) {
    // ...
            <form onSubmit={e => { e.preventDefault(); dispatch({ type: 'LOGOUT' }) }}>

现在,我们的应用应该和以前一样工作,但是它使用了 Reducer Hook 而不是简单的 State Hook!

替换 posts State Hook

使用 Reducer Hook 来处理posts状态也是有道理的,因为以后我们会有一些功能可以用来删除和编辑帖子,所以将这些复杂的状态变化封装起来是很有意义的。现在让我们开始用 Reducer Hook 替换 posts State Hook。

定义操作

同样,我们首先定义操作。目前,我们只考虑CREATE_POST操作:

{ type: 'CREATE_POST', title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' }

这是我们目前需要的唯一操作。

定义 reducer

接下来,我们将以与user状态相似的方式定义 reducer 函数:

  1. 我们首先编辑src/App.js,在那里定义 reducer 函数。以下代码定义了postsReducer函数:
function postsReducer (state, action) {
    switch (action.type) {
  1. 在这个函数中,我们将处理CREATE_POST操作。我们首先创建一个newPost对象,然后使用扩展语法将其插入到当前posts状态的开头,类似于我们之前在src/post/CreatePost.js组件中所做的方式:
        case 'CREATE_POST':
            const newPost = { title: action.title, content: action.content, author: action.author }
            return [ newPost, ...state ]
  1. 目前,这将是我们在这个 reducer 中处理的唯一操作,所以我们现在可以定义default语句:
        default:
            throw new Error()
    }
}

现在,postsReducer函数已经定义,我们可以继续创建 Reducer Hook。

定义 Reducer Hook

最后,我们将定义并使用posts状态的 Reducer Hook:

  1. 我们首先在src/App.js中删除以下 State Hook:
       const [ posts, setPosts ] = useState(defaultPosts)

我们用以下 Reducer Hook 替换它:

       const [ posts, dispatchPosts ] = useReducer(postsReducer, defaultPosts)
  1. 然后,我们将dispatchPosts函数作为dispatch属性传递给CreatePost组件:
            {user && <CreatePost user={user} posts={posts} dispatch={dispatchPosts} />}
  1. 接下来,我们编辑src/post/CreatePost.js中的CreatePost组件,并用dispatch函数替换setPosts函数:
export default function CreatePost ({ user, posts, dispatch }) {
  1. 最后,在handleCreate函数中使用dispatch函数:
    function handleCreate () {
        dispatch({ type: 'CREATE_POST', title, content, author: user })
    }

现在,posts状态也使用 Reducer Hook 而不是 State Hook,并且与以前的方式一样工作!然而,如果以后我们想要添加更多逻辑来管理帖子,比如搜索、过滤、删除和编辑,那么这将更容易做到。

示例代码

在我们的博客应用程序中使用 Reducer Hook 的示例代码可以在Chapter04/chapter4_1文件夹中找到。

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

合并 Reducer Hook

目前,我们有两个不同的 dispatch 函数:一个用于user状态,一个用于posts状态。在我们的情况下,将这两个 reducers 合并成一个是有意义的,然后调用进一步的 reducers 来处理子状态。

这种模式类似于 Redux 中 reducer 的工作方式,其中我们只有一个包含整个应用程序状态树的对象,在全局状态的情况下是有意义的。然而,对于复杂的局部状态更改,将 reducers 保持分开可能更有意义。

让我们开始将我们的 reducer 函数合并成一个 reducer 函数。在此过程中,让我们将所有 reducers 重构到src/reducers.js文件中,以使src/App.js文件更易读:

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

  2. src/App.js文件中剪切以下代码,并粘贴到src/reducers.js文件中:

function userReducer (state, action) {
    switch (action.type) {
        case 'LOGIN':
        case 'REGISTER':
            return action.username

        case 'LOGOUT':
            return ''

        default:
            throw new Error()
    }
}

function postsReducer (state, action) {
    switch (action.type) {
        case 'CREATE_POST':
            const newPost = { title: action.title, content: action.content, author: action.author }
            return [ newPost, ...state ]

        default:
            throw new Error()
    }
}
  1. 编辑src/reducers.js,并在现有的 reducer 函数下面定义一个新的 reducer 函数,名为appReducer
export default function appReducer (state, action) {
  1. 在这个appReducer函数中,我们将调用另外两个 reducer 函数,并返回完整的状态树:
    return {
        user: userReducer(state.user, action),
        posts: postsReducer(state.posts, action)
    }
}
  1. 编辑src/App.js,并在那里导入appReducer
import  appReducer  from  './reducers'
  1. 然后,我们移除以下两个 Reducer Hook 定义:
            const [ user, dispatchUser ] = useReducer(userReducer,
             '')
            const [ posts, dispatchPosts = useReducer(postsReducer, 
         defaultPosts)

appReducer的单一 Reducer Hook 定义替换前面的 Reducer Hook 定义:

    const [ state, dispatch ] = useReducer(appReducer, { user: '', posts: defaultPosts })
  1. 接下来,我们使用解构从我们的state对象中提取userposts的值:
    const { user, posts } = state
  1. 现在,我们仍然需要用dispatch函数替换我们传递给其他组件的dispatchUserdispatchPosts函数:
            <UserBar user={user} dispatch={dispatch} />
            <br />
            {user && <CreatePost user={user} posts={posts} dispatch={dispatch} />}

我们可以看到,现在只有一个dispatch函数和一个单一的状态对象。

忽略未处理的 actions

然而,如果我们现在尝试登录,我们将会看到来自postsReducer的错误。这是因为我们仍然在未处理的 actions 上抛出错误。为了避免这种情况,我们必须忽略未处理的 actions,简单地返回当前状态:

编辑src/reducers.js中的userReducerpostsReducer函数,并移除以下代码:

        default:
            throw new Error()

用一个return语句替换前面的代码,该语句返回当前的state

            default:
                return state

我们可以看到,现在我们的应用程序仍然以与以前完全相同的方式工作,但我们正在使用一个单一的 reducer 来处理整个应用程序状态!

示例代码

我们博客应用程序中使用单一 Reducer Hook 的示例代码可以在Chapter04/chapter4_2文件夹中找到。

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

使用 Effect Hooks

我们将经常使用的最后一个重要 Hook 是 Effect Hook。 使用 Effect Hook,我们可以从我们的组件执行副作用,例如在组件挂载或更新时获取数据。

在我们的博客案例中,我们将实现一个功能,当我们登录时更新我们网页的标题,以便包含当前登录用户的用户名。

记得 componentDidMount 和 componentDidUpdate 吗?

如果您以前使用过 React,您可能已经使用了componentDidMountcomponentDidUpdate生命周期方法。 例如,我们可以使用 React 类组件将文档title设置为给定的 prop,如下所示。 在下面的代码部分中,生命周期方法用粗体标出:

import React from 'react'

class App extends React.Component {
 componentDidMount () {
 const { title } = this.props document.title = title
 }

    render () {
        return (
            <div>Test App</div>
        )
    }
}

这很好。 但是,当titleprop 更新时,更改不会反映在我们网页的标题中。 为了解决这个问题,我们需要定义componentDidUpdate生命周期方法(新代码用粗体标出),如下所示:

import React from 'react'

class App extends React.Component {
    componentDidMount () {
        const { title } = this.props
        document.title = title
    }

 componentDidUpdate (prevProps) {
 const { title } = this.props
        if (title !== prevProps.title) {
 document.title = title
        }
 }

    render () {
        return (
            <div>Test App</div>
        )
    }
}

您可能已经注意到我们几乎写了相同的代码两次;因此,我们可以创建一个新方法来处理title的更新,然后从两个生命周期方法中调用它。 在下面的代码部分中,更新的代码用粗体标出:

import React from 'react'

class App extends React.Component {
 updateTitle () {
 const { title } = this.props
 document.title = title
 }

    componentDidMount () {
        this.updateTitle()
    }

    componentDidUpdate (prevProps) {
        if (this.props.title !== prevProps.title) {
 this.updateTitle()
        }
    }

    render () {
        return (
            <div>Test App</div>
        )
    }
}

但是,我们仍然需要两次调用this.updateTitle()。 当我们稍后更新代码时,例如,向this.updateTitle()传递参数时,我们始终需要记住在两次调用方法时传递它。 如果我们忘记更新其中一个生命周期方法,可能会引入错误。 此外,我们需要在componentDidUpdate中添加一个if条件,以避免在titleprop 未更改时调用this.updateTitle()

使用 Effect Hook

在 Hooks 的世界中,componentDidMountcomponentDidUpdate生命周期方法在useEffectHook 中合并在一起,当不指定依赖数组时,会在组件中的任何 prop 更改时触发。

因此,我们现在可以使用 Effect Hook 定义一个带有 Effect Hook 的函数组件,它与以前的功能相同。 传递给 Effect Hook 的函数称为“effect 函数”:

import React, { useEffect } from 'react'

function App ({ title }) {
    useEffect(() => {
        document.title = title
    })

    return (
        <div>Test App</div>
    )
}

这就是我们需要做的一切! 我们定义的 Hook 将在任何 prop 更改时调用我们的 effect 函数。

仅在某些 props 更改时触发效果

如果我们想要确保我们的效果函数只在title prop 发生变化时才被调用,例如出于性能原因,我们可以指定应该触发更改的值,作为useEffect Hook 的第二个参数:

    useEffect(() => {
        document.title = title
    }, [title])

而且这不仅限于 props,我们可以在这里使用任何值,甚至来自其他 Hooks 的值,比如 State Hook 或 Reducer Hook:

    const [ title, setTitle ] = useState('')
    useEffect(() => {
        document.title = title
    }, [title])

正如我们所看到的,使用 Effect Hook 比使用生命周期方法处理变化的值要简单得多。

仅在挂载时触发效果

如果我们想要复制仅添加componentDidMount生命周期方法的行为,而不在 props 更改时触发,我们可以通过将空数组作为useEffect Hook 的第二个参数来实现这一点:

    useEffect(() => {
        document.title = title
    }, [])

传递一个空数组意味着我们的效果函数只会在组件挂载时触发一次,并且不会在 props 更改时触发。然而,与其考虑组件的挂载,使用 Hooks,我们应该考虑效果的依赖关系。在这种情况下,效果没有任何依赖关系,这意味着它只会触发一次。如果一个效果有指定的依赖关系,当任何依赖关系发生变化时,它将再次触发。

清理效果

有时效果在组件卸载时需要清理。为此,我们可以从 Effect Hook 的效果函数中返回一个函数。这个返回的函数类似于componentWillUnmount生命周期方法:

    useEffect(() => {
        const updateInterval = setInterval(() => console.log('fetching update'), updateTime)

        return () => clearInterval(updateInterval)
    }, [updateTime])

上面加粗的代码被称为清理函数。清理函数将在组件卸载时调用,并在再次运行效果之前调用。这可以避免 bug,例如updateTime prop 发生变化。在这种情况下,先前的效果将被清理,并且将使用更新的updateTime值定义一个新的间隔。

在我们的博客应用程序中实现一个 Effect Hook

现在我们已经学会了 Effect Hook 的工作原理,我们将在我们的博客应用程序中使用它,以在登录/注销时实现标题更改(当user状态发生变化时)。

让我们开始在我们的博客应用程序中实现一个 Effect Hook:

  1. 编辑src/App.js,并导入useEffect Hook:
import React, { useReducer, useEffect } from 'react'
  1. 在定义了我们的useReducer Hook 和状态解构之后,定义一个useEffect Hook,根据username值调整document.title变量:
    useEffect(() => {
  1. 如果用户已登录,我们将document.title设置为<username> - React Hooks Blog。我们使用模板字符串,允许我们通过${ }语法在字符串中包含变量或 JavaScript 表达式。模板字符串使用`定义:
        if (user) {
            document.title = `${user} - React Hooks Blog`
  1. 否则,如果用户没有登录,我们只需将document.title设置为React Hooks Blog即可:
        } else {
        document.title = 'React Hooks Blog'
        }
  1. 最后,我们将user值作为第二个参数传递给效果挂钩,以确保每当user值更新时,我们的效果函数都会再次触发:
    }, [user])

如果我们现在启动我们的应用程序,我们可以看到document.title被设置为React Hooks Blog,因为 Effect Hook 在App组件挂载时触发,而user值尚未定义:

我们的 Effect Hook 的效果:改变网页标题

在使用Test User登录后,我们可以看到document.title更改为Test User - React Hooks Blog

我们的 Effect Hook 重新触发的效果,用户值更改后

正如我们所看到的,我们的 Effect Hook 在user值更改后成功重新触发!

示例代码

在我们的博客应用程序中实现 Effect Hooks 的示例代码可以在Chapter04/chapter4_3文件夹中找到。

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

总结

在本章中,我们首先学习了 actions、reducers 和 Reducer Hooks。我们还学习了何时应该使用 Reducer Hooks 而不是 State Hooks。然后,我们用两个 Reducer Hooks 替换了现有的全局 State Hooks,用于userposts状态。接下来,我们将两个 Reducer Hooks 合并为一个单一的 app Reducer Hook。最后,我们学习了 Effect Hooks,以及它们如何可以代替componentDidMountcomponentDidUpdate

在下一章中,我们将学习关于 React context,以及如何在 Hooks 中使用它。然后,我们将向我们的应用程序添加 Context Hooks,以避免在多个组件层中传递 props。

问题

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

  1. State Hooks 有哪些常见问题?

  2. 什么是 actions?

  3. 什么是 reducer?

  4. 何时应该使用 Reducer Hook 而不是 State Hook?

  5. 将 State Hook 转换为 Reducer Hook 需要哪些步骤?

  6. 我们如何更轻松地创建 actions?

  7. 何时应该合并 Reducer Hooks?

  8. 在合并 Reducer Hooks 时需要注意什么?

  9. 在类组件中,等效于 Effect Hook 的是什么?

  10. 使用 Effect Hook 与类组件相比有哪些优势?

进一步阅读

如果您对本章探讨的概念更感兴趣,可以查阅以下阅读材料:

第五章:实现 React 上下文

在之前的章节中,我们学习了最基本的 Hooks,比如 State Hook、Reducer Hook 和 Effect Hook。我们使用这些 Hooks 开发了一个小型的博客应用程序。在开发博客应用程序的过程中,我们注意到我们不得不从App组件传递user状态到UserBar组件,然后从UserBar组件传递到LoginRegisterLogout组件。为了避免这样传递状态,我们现在要学习关于 React 上下文和上下文 Hooks。

我们将首先学习什么是 React 上下文,提供者和消费者是什么。然后,我们将使用上下文 Hooks 作为上下文消费者,并讨论何时应该使用上下文。最后,我们将通过上下文实现主题和全局状态。

本章将涵盖以下主题:

  • 引入 React 上下文作为传递 props 的替代方法

  • 通过上下文实现主题

  • 利用上下文来管理全局状态

技术要求

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

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

查看以下视频以查看代码的实际操作:

bit.ly/2Mm9yoC

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

现在,让我们开始本章。

介绍 React 上下文

在之前的章节中,我们从App组件传递了user状态和dispatch函数到UserBar组件;然后从UserBar组件传递到LogoutLoginRegister组件。React 上下文提供了解决这种繁琐的跨多个组件级别传递 props 的方法,它允许我们在组件之间共享值,而无需通过 props 显式传递它们。正如我们将看到的,React 上下文非常适合在整个应用程序中共享值。

首先,我们将更仔细地看一下传递 props 的问题。然后,我们将介绍 React 上下文作为解决方案。

传递 props

在深入学习 React 上下文之前,让我们回顾一下我们在之前章节中实现的内容,以便对上下文解决的问题有所了解:

  1. src/App.js中,我们定义了user状态和dispatch函数:
 const  [  state,  dispatch  ]  =  useReducer(appReducer,  { user:  '', posts:  defaultPosts  })
 const  {  user,  posts  }  =  state
  1. 然后,我们将user状态和dispatch函数传递给UserBar组件(以及CreatePost组件):
    return ( <div  style={{ padding: 8 }}> <UserBar  user={user} dispatch={dispatch**}** /> <br /> {user  && <CreatePost  user={user} posts={posts} dispatch={dispatch**}** />} <br /> <hr /> <PostList  posts={posts} /> </div> )
  1. src/user/UserBar.js组件中,我们将user状态作为 prop,并将其传递给Logout组件。我们还将dispatch函数作为 prop,并将其传递给LogoutLoginRegister组件:
export  default  function  UserBar  ({  user,  dispatch  })  { if (user) { return  <Logout  user={user} dispatch={dispatch**}** /> }  else  { return ( <React.Fragment> <Login  dispatch={dispatch**}** /> <Register  dispatch={dispatch**}** /> </React.Fragment>
        ) } }
  1. 最后,我们在LogoutLoginRegister组件中使用了dispatchuserprops。

React 上下文允许我们跳过步骤 2 和 3,直接从步骤 1 跳到步骤 4。可以想象,对于更大的应用程序,上下文变得更加有用,因为我们可能需要在许多级别上传递 props。

介绍 React 上下文

React 上下文用于在 React 组件树中共享值。通常,我们希望共享全局值,例如user状态和dispatch函数,应用程序的主题或所选择的语言。

React 上下文由两部分组成:

  • 提供者,提供(设置)值

  • 消费者,消耗(使用)值

首先,我们将看一下上下文是如何工作的,使用一个简单的例子,然后在下一节中,我们将在我们的博客应用中实现它们。我们使用create-react-app工具创建一个新项目。在我们的简单示例中,我们将定义一个主题上下文,其中包含应用程序的主要颜色。

定义上下文

首先,我们必须定义上下文。自从引入 Hooks 以来,这种工作方式并没有改变。

我们只需使用React.createContext(defaultValue)函数创建一个新的上下文对象。我们将默认值设置为{ primaryColor: 'deepskyblue' },因此当没有提供者定义时,我们的默认主要颜色将是'deepskyblue'

src/App.js中,在App函数之前添加以下定义:

export const ThemeContext = React.createContext({ primaryColor: 'deepskyblue' })

请注意,我们在这里导出ThemeContext,因为我们将需要导入它作为消费者。

这就是我们使用 React 定义上下文所需做的一切。现在我们只需要定义消费者。

定义消费者

现在,我们必须在我们的Header组件中定义消费者。现在我们将以传统方式做这个,然后在下一步中使用 Hooks 来定义消费者:

  1. 创建一个新的src/Header.js文件

  2. 首先,我们必须从App.js文件中导入ThemeContext

import React from 'react'
import { ThemeContext } from './App'
  1. 现在,我们可以定义我们的组件,在这里我们使用ThemeContext.Consumer组件和一个render函数作为children属性,以便利用上下文值:
const Header = ({ text }) => (
    <ThemeContext.Consumer>
        {theme => (
  1. render函数中,我们现在可以利用上下文值来设置我们的Header组件的color样式:

            <h1 style={{ color: theme.primaryColor }}>{text}</h1>
        )}
    </ThemeContext.Consumer>
)

export default Header
  1. 现在,我们仍然需要在src/App.js中导入Header组件,通过添加以下import语句:
import Header from './Header'
  1. 然后,我们用以下代码替换当前的App函数:
const App = () => (
    <Header text="Hello World" />
)

export default App

像这样使用上下文是有效的,但是,正如我们在第一章中学到的那样,以这种方式使用带有render函数 props 的组件会使我们的 UI 树混乱,并使我们的应用程序更难以调试和维护。

使用 Hooks

使用上下文的更好方法是使用useContext Hook!这样,我们可以像使用useState Hook 一样使用上下文值:

  1. 编辑src/Header.js。首先,我们从 React 中导入useContext Hook,以及从src/App.js中导入ThemeContext对象:
import React, { useContext } from 'react'
import { ThemeContext } from './App'
  1. 然后,我们创建我们的Header组件,现在我们定义useContext Hook:
const Header = ({ text }) => {
 const theme = useContext(ThemeContext)
  1. 我们组件的其余部分将与以前相同,只是现在,我们可以简单地返回我们的Header组件,而不需要使用额外的组件来作为消费者:
    return <h1 style={{ color: theme.primaryColor }}>{text}</h1>
}

export default Header

正如我们所看到的,使用 Hooks 使我们的上下文消费者代码更加简洁。此外,它将更容易阅读,维护和调试。

我们可以看到标题现在的颜色是deepskyblue

一个使用上下文 Hook 的简单应用程序!

正如我们所看到的,我们的主题上下文成功为标题提供了主题。

定义提供程序

当没有定义提供程序时,上下文使用传递给React.createContext的默认值。当组件没有嵌入在应用程序中时,这对于调试组件非常有用。例如,我们可以调试单个组件作为独立组件。在应用程序中,我们通常希望使用提供程序来提供上下文的值,我们现在将定义它。

编辑src/App.js,在我们的App函数中,我们简单地用<ThemeContext.Provider>组件包装Header组件,其中我们将coral作为primaryColor传递:

const App = () => (
    <ThemeContext.Provider value={{ primaryColor: 'coral' }}>
        <Header text="Hello World" />
    </ThemeContext.Provider>
)

export default App

我们现在可以看到我们的标题颜色从 deepskyblue 变为 coral

我们的提供者改变了标题的颜色

如果我们想要更改上下文的值,我们可以简单地调整传递给Provider组件的value属性。

请注意,当我们在没有将value属性传递给它的情况下定义提供者时,上下文的默认值不会被使用!如果我们定义一个没有value属性的提供者,那么上下文的值将是undefined

现在我们已经为我们的上下文定义了单个提供者,让我们继续定义多个嵌套提供者。

嵌套提供者

使用 React 上下文,还可以为同一上下文定义多个提供者。使用这种技术,我们可以在应用程序的某些部分覆盖上下文值。让我们考虑之前的例子,并向其添加第二个标题:

  1. 编辑 src/App.js,并添加第二个 Header 组件:
const App = () => (
    <ThemeContext.Provider value={{ primaryColor: 'coral' }}>
        <Header text="Hello World" />
 <Header text="This is a test" />
    </ThemeContext.Provider>
)

export default App
  1. 现在,使用不同的primaryColor定义第二个Provider组件:
const App = () => (
    <ThemeContext.Provider value={{ primaryColor: 'coral' }}>
        <Header text="Hello World" />
 <ThemeContext.Provider value={{ primaryColor: 'deepskyblue' }}> <Header text="This is a test" />
        </ThemeContext.Provider>
    </ThemeContext.Provider>
)

export default App

如果我们在浏览器中打开应用程序,第二个标题现在与第一个标题的颜色不同:

使用嵌套提供者覆盖上下文值

正如我们所看到的,我们可以通过定义提供者来覆盖 React 上下文的值。提供者也可以嵌套,因此可以覆盖组件树中较高提供者的值。

示例代码

小主题上下文示例的示例代码可以在Chapter05/chapter5_1文件夹中找到。

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

上下文的替代方案

但是,我们应该小心,不要经常使用 React 上下文,因为这会使组件的重用变得更加困难。我们只应在需要在许多不同嵌套级别的组件中访问数据时使用上下文。此外,我们需要确保只使用上下文来存储不经常更改的数据。上下文频繁更改的值可能导致整个组件树重新渲染,从而导致性能问题。因此,对于频繁更改的值,我们应该使用 Redux 或 MobX 等状态管理解决方案。

如果我们只想避免传递 props,我们可以传递渲染的组件而不是数据。例如,假设我们有一个Page组件,它渲染一个Header组件,后者又渲染一个Profile组件,然后渲染一个Avatar组件。我们在Page组件中得到一个headerSize属性,我们需要在Header组件中使用它,但也需要在Avatar组件中使用它。我们可以这样做,而不是通过多个级别传递 props:

function Page ({ headerSize }) {
    const profile = (
        <Profile>
            <Avatar size={headerSize} />
        </Profile>
    )
    return <Header size={headerSize} profile={profile} />
}

现在,只有Page组件需要知道headerSize属性,而且不需要在树中进一步传递。在这种情况下,上下文是不必要的。

这种模式被称为控制反转,它可以使您的代码比传递 props 或使用上下文更清晰。然而,我们也不应该总是使用这种模式,因为它会使高级组件变得更加复杂。

实现主题

在学习了如何在一个小例子中实现主题之后,我们现在要在我们的博客应用程序中使用 React 上下文和钩子来实现主题。

定义上下文

首先,我们必须定义上下文。在我们的博客应用程序中,我们将创建一个单独的文件来定义上下文,而不是在src/App.js文件中定义它。将上下文放在单独的文件中可以更容易地在以后进行维护。此外,我们总是知道从哪里导入上下文,因为文件名清晰明了。

让我们开始定义一个主题上下文:

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

  2. 然后,我们导入React

import React from 'react'
  1. 接下来,我们定义ThemeContext。与我们在小例子中一样,我们将默认的primaryColor设置为deepskyblue。此外,我们将secondaryColor设置为coral
export const ThemeContext = React.createContext({
    primaryColor: 'deepskyblue',
    secondaryColor: 'coral'
})

现在我们已经定义了上下文,我们可以继续定义上下文钩子。

定义上下文钩子

在定义上下文之后,我们将使用上下文钩子来定义我们的消费者。我们首先创建一个新的头部组件,然后为现有的Post组件定义一个上下文钩子。

创建头部组件

首先,我们创建一个新的Header组件,它将在我们应用程序的primaryColor中显示React Hooks Blog

现在让我们创建Header组件:

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

  2. 在这个文件中,我们导入ReactuseContext钩子:

import React, { useContext } from 'react'
  1. 接下来,我们从先前创建的src/contexts.js文件中导入ThemeContext
import { ThemeContext } from `'./contexts'
  1. 然后,我们定义我们的Header组件和上下文钩子。我们不再将上下文值存储在theme变量中,而是使用解构直接提取primaryColor值:
const Header = ({ text }) => {
    const { primaryColor } = useContext(ThemeContext)
  1. 最后,我们返回h1元素,就像我们在我们的小例子中做的那样,并export Header组件:
    return <h1 style={{ color: primaryColor }}>{text}</h1>
}

export default Header

现在我们已经定义了Header组件,我们可以使用它了。

使用Header组件

创建Header组件后,我们将在App组件中使用它,如下所示:

  1. 编辑src/App.js,并导入Header组件:
import Header from './Header'
  1. 然后,在UserBar组件之前呈现Header组件:
    return (
        <div style={{ padding: 8 }}>
            <Header text="React Hooks Blog" />
            <UserBar user={user} dispatch={dispatch} />

您可能希望将React Hooks Blog值重构为传递给App组件的 prop(应用程序配置),因为我们在这个组件中已经使用了三次。

现在,我们的Header组件将在应用程序中呈现,我们可以继续在Post组件中实现上下文钩子。

实现Post组件的上下文钩子

接下来,我们希望用辅助颜色显示Post标题。为此,我们需要为Post组件定义一个上下文钩子,如下所示:

  1. 编辑src/post/Post.js,并调整import语句以导入useContext钩子:
import React, { useContext } from 'react'
  1. 接下来,我们导入ThemeContext
import { ThemeContext } from '../contexts'
  1. 然后,在Post组件中定义一个上下文钩子,并通过解构从主题中获取secondaryColor值:
export  default  function  Post  ({  title,  content,  author  })  {
    **const { secondaryColor } = useContext(ThemeContext)** 
  1. 最后,我们使用secondaryColor值来设置我们的h3元素的样式:
    return (
        <div>
            <h3 style={{ color: secondaryColor }}>{title}</h3>

如果我们现在查看我们的应用程序,我们可以看到ThemeContext中两种颜色都被正确使用:

我们的 ThemeContext 在行动

正如我们所看到的,我们的应用程序现在使用主标题的主要颜色,以及帖子标题的辅助颜色。

定义提供程序

现在,我们的上下文钩子使用上下文指定的默认值,当没有定义提供程序时。为了能够更改值,我们需要定义一个提供程序。

让我们开始定义提供程序:

  1. 编辑src/App.js,并导入ThemeContext
import { ThemeContext } from './contexts'
  1. ThemeContext.Provider组件包装整个应用程序,提供与我们之前设置为默认值的相同主题:
    return (
 <ThemeContext.Provider value={{ primaryColor: 'deepskyblue', secondaryColor: 'coral' }}> <div style={{ padding: 8 }}>
                <Header text="React Hooks Blog" />
                ...
                <PostList posts={posts} />
            </div>
 </ThemeContext.Provider>
    )

我们的应用程序应该看起来和以前完全一样,但现在我们正在使用提供程序中的值!

动态更改主题

现在我们已经定义了一个提供程序,我们可以使用它来动态改变主题。我们将不再向提供程序传递静态值,而是使用一个 State Hook 来定义当前主题。然后,我们将实现一个改变主题的组件。

使用上下文提供程序的 State Hook

首先,我们将定义一个新的 State Hook,我们将使用它来设置上下文提供程序的值。

让我们定义一个 State Hook,并在上下文提供程序中使用它:

  1. 编辑src/App.js,并导入useState Hook:
import React, { useReducer, useEffect, useState } from 'react'
  1. App组件的开头定义一个新的 State Hook;在这里,我们将默认值设置为我们的默认主题:
export default function App () {
 const [ theme, setTheme ] = useState({
 primaryColor: 'deepskyblue',
 secondaryColor: 'coral'
 })
  1. 然后,我们将theme值传递给ThemeContext.Provider组件:
    return (
        <ThemeContext.Provider value={theme}>

我们的应用程序看起来仍然与以前一样,但现在我们已经准备好动态改变我们的主题了!

实现 ChangeTheme 组件

我们主题功能的最后部分是一个组件,可以通过利用我们之前定义的 State Hook 来动态改变主题。State Hook 将重新渲染App组件,这将改变传递给ThemeContext.Provider的值,进而重新渲染所有使用ThemeContext Context Hook 的组件。

让我们开始实现ChangeTheme组件:

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

  2. 和往常一样,我们必须先导入React,然后才能定义一个组件:

import React from 'react'
  1. 为了能够轻松地以后添加新的主题,我们将创建一个常量THEMES数组,而不是手动复制和粘贴不同主题的代码。这将使我们的代码更加简洁,更易于阅读:
const THEMES = [
    { primaryColor: 'deepskyblue', secondaryColor: 'coral' },
    { primaryColor: 'orchid', secondaryColor: 'mediumseagreen' }
]

给硬编码的常量值一个特殊的名称是个好主意,比如用大写字母写整个变量名。以后,将所有这些可配置的硬编码值放在一个单独的src/config.js文件中可能是有意义的。

  1. 接下来,我们定义一个组件来渲染单个theme
function ThemeItem ({ theme, active, onClick }) {
  1. 在这里,我们渲染一个链接,并显示主题的小预览,显示主要颜色和次要颜色:
    return (
        <span onClick={onClick} style={{ cursor: 'pointer', paddingLeft: 8, fontWeight: active ? 'bold' : 'normal' }}>
            <span style={{ color: theme.primaryColor }}>Primary</span> / <span style={{ color: theme.secondaryColor }}>Secondary</span>
        </span>
    )
}

在这里,我们将光标设置为pointer,以使元素看起来可点击。我们也可以使用<a>元素;但是,如果我们没有有效的链接目标,比如一个单独的页面,这是不推荐的。

  1. 然后,我们定义ChangeTheme组件,它接受themesetTheme props:
export default function ChangeTheme ({ theme, setTheme }) {
  1. 接下来,我们定义一个函数来检查一个主题对象是否是当前活动的主题:
    function isActive (t) {
        return t.primaryColor === theme.primaryColor && t.secondaryColor === theme.secondaryColor
    }
  1. 现在,我们使用.map函数来渲染所有可用的主题,并在点击它们时调用setTheme函数:
    return (
        <div>
            Change theme:
            {THEMES.map((t, i) =>
                <ThemeItem key={'theme-' + i} theme={t} active={isActive(t)} onClick={() => setTheme(t)} />
            )}
        </div>
    )
}
  1. 最后,在src/App.js中的Header组件之后导入并渲染ChangeTheme组件:
import ChangeTheme from './ChangeTheme'
// ...
    return (
        <ThemeContext.Provider value={theme}>
            <div style={{ padding: 8 }}>
                <Header text="React Hooks Blog" />
                <ChangeTheme theme={theme} setTheme={setTheme} />
                <br /> 

我们可以看到,我们现在有一种方法可以在我们的应用程序中更改主题:

我们在更改主题后,使用上下文钩子与状态钩子相结合

现在,我们有一个通过 Hooks 消耗的上下文,也可以通过 Hooks 进行更改!

示例代码

我们的博客应用程序中主题功能的示例代码可以在Chapter05/chapter5_2文件夹中找到。

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

使用上下文进行全局状态

在学习了如何使用 React 上下文在我们的博客应用程序中实现主题之后,我们现在将使用上下文来避免手动传递statedispatch props 以用于全局应用程序状态。

定义 StateContext

我们首先在我们的src/contexts.js文件中定义上下文。

src/contexts.js中,我们定义了StateContext,它将存储state值和dispatch函数:

export const StateContext = React.createContext({
    state: {},
    dispatch: () => {}
})

我们将state值初始化为空对象,并将dispatch函数初始化为空函数,当没有提供程序时将使用它。

定义上下文提供程序

现在,我们将在我们的src/App.js文件中定义上下文提供程序,该提供程序将从现有的 Reducer Hook 中获取值。

现在让我们为全局状态定义上下文提供程序:

  1. src/App.js中,通过调整现有的import语句导入StateContext
import  {  ThemeContext**, StateContext**  }  from  './contexts'
  1. 然后,我们通过从我们的App函数中返回它来定义一个新的上下文提供程序:
    return (
 <StateContext.Provider value={{ state, dispatch }}>
            <ThemeContext.Provider value={theme}>
                ...
            </ThemeContext.Provider>
 </StateContext.Provider>
    )

现在,我们的上下文提供程序为我们的整个应用程序提供了state对象和dispatch函数,我们可以继续使用上下文值。

使用 StateContext

现在,我们已经定义了我们的上下文和提供程序,我们可以在各种组件中使用state对象和dispatch函数。

我们首先删除在src/App.js中手动传递给组件的 props。删除以下用粗体标记的代码段:

        <div style={{ padding: 8 }}>
            <Header text="React Hooks Blog" />
            <ChangeTheme theme={theme} setTheme={setTheme} />
            <br />
            <UserBar user={user} dispatch={dispatch} />
            <br />
            {user && <CreatePost user={user} posts={posts} dispatch={dispatch} />}
            <br />
            <hr />
            <PostList posts={posts} />
        </div>

由于我们正在使用上下文,因此不再需要手动传递 props。我们现在可以继续重构组件。

重构用户组件

首先,我们重构用户组件,然后再转向帖子组件。

现在让我们重构与用户相关的组件:

  1. 编辑src/user/UserBar.js,并且移除那里的 props(加粗标记的代码应该被移除),因为我们不再需要手动传递它们了:
export  default  function  UserBar  (**{** user,  dispatch **}**)  {
 if (user) { return  <Logout  user={user} dispatch={dispatch**}** /> }  else  { return ( <React.Fragment> <Login  dispatch={dispatch**}** /> <Register  dispatch={dispatch**}** /> </React.Fragment> ) } } 
  1. 然后,我们在src/user/UserBar.js中导入useContext Hook 和StateContext,以便能够判断用户是否已登录:
import React, { useContext } from 'react'
import { StateContext } from '../contexts'
  1. 现在,我们可以使用 Context Hook 从我们的state对象中获取user状态:
export default function UserBar () {
 const { state } = useContext(StateContext)
 const { user } = state
  1. 同样地,我们在src/user/Login.js中导入useContextStateContext
import React, { useState, useContext } from 'react'
import { StateContext } from '../contexts'
  1. 然后,我们移除dispatch prop,并使用 Context Hook 代替:
export default function Login () {
 const { dispatch } = useContext(StateContext)
  1. 我们在src/user/Register.js组件中重复相同的过程:
import React, { useState, useContext } from 'react'
import { StateContext } from '../contexts' export default function Register () { const { dispatch } = useContext(StateContext) 
  1. src/user/Logout.js组件中,我们做同样的事情,但也从state对象中获取user状态:
import React, { useContext } from 'react'
import { StateContext } from '../contexts' export default function Logout () { const { state, dispatch } = useContext(StateContext)
    const { user } = state 

我们的与用户相关的组件现在使用上下文而不是 props。让我们继续重构与帖子相关的组件。

重构帖子组件

现在,唯一剩下的就是重构帖子组件;然后我们整个应用程序都将使用 React 上下文来管理全局状态:

  1. 我们从src/post/PostList.js组件开始,导入useContextStateContext,移除 props,并使用 Context Hook 代替:
import React, { useContext } from 'react'
import { StateContext } from '../contexts'

import Post from './Post'

export default function PostList () {
 const { state } = useContext(StateContext)
 const { posts } = state
  1. 我们对CreatePost组件做同样的事情,这是我们需要重构的最后一个组件:
import React, { useState, useContext } from 'react'
import { StateContext } from '../contexts'

export default function CreatePost () {
 const { state, dispatch } = useContext(StateContext)
 const { user } = state

我们的应用程序的工作方式与以前相同,但现在我们使用上下文来管理全局状态,这使我们的代码更清晰,并避免了必须传递 props!

示例代码

我们博客应用程序中全局状态上下文的示例代码可以在Chapter05/chapter5_3文件夹中找到。

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

总结

在本章中,我们首先学习了 React 上下文作为在多个级别的 React 组件之间传递 props 的替代方法。然后,我们学习了上下文提供者和消费者,以及通过 Hooks 定义消费者的新方法。接下来,我们学习了何时不应该使用上下文,以及何时应该使用控制反转。然后,我们通过在博客应用程序中实现主题来实践所学的知识。最后,我们在博客应用程序中使用 React 上下文进行全局状态管理。

在下一章中,我们将学习如何使用 React 和 Hooks 从服务器请求数据。然后,我们将学习React.memo来防止组件不必要地重新渲染,以及 React Suspense 来在需要时延迟加载组件。

问题

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

  1. 上下文避免了哪些问题?

  2. 上下文由哪两部分组成?

  3. 使用上下文时,两部分都必须定义吗?

  4. 使用 Hooks 而不是传统上下文消费者的优势是什么?

  5. 上下文的替代方案是什么,何时应该使用它?

  6. 我们如何实现动态更改上下文?

  7. 何时使用上下文来管理状态是有意义的?

进一步阅读

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

第六章:实现请求和 React Suspense

在之前的章节中,我们学习了如何使用 React 上下文作为手动传递 props 的替代方法。我们了解了上下文提供者、消费者以及如何使用 Hooks 作为上下文消费者。接下来,我们学习了作为上下文替代方法的控制反转。最后,我们在博客应用程序中使用上下文实现了主题和全局状态。

在本章中,我们将设置一个简单的后端服务器,该服务器将从JavaScript 对象表示JSON)文件中生成,使用json-server工具。然后,我们将通过使用 Effect Hook 结合 State Hook 来实现请求资源。接下来,我们将使用axiosreact-request-hook库做同样的事情。最后,我们将通过使用React.memo来防止不必要的重新渲染,并通过 React Suspense 来懒加载组件。

本章将涵盖以下主题:

  • 使用 Hooks 请求资源

  • 使用React.memo防止不必要的重新渲染

  • 使用 React Suspense 实现延迟加载

技术要求

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

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

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

bit.ly/2Mm9yoC

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

现在,让我们开始本章。

使用 Hooks 请求资源

在本节中,我们将学习如何使用 Hooks 从服务器请求资源。首先,我们将只使用 JavaScript 的fetch函数和useEffect/useState Hooks 来实现请求。然后,我们将学习如何使用axios库结合react-request-hook来请求资源。

设置虚拟服务器

在我们实现请求之前,我们需要创建一个后端服务器。由于我们目前专注于用户界面,我们将设置一个虚拟服务器,这将允许我们测试请求。我们将使用json-server工具从 JSON 文件创建一个完整的表述状态转移REST)API。

创建 db.json 文件

为了能够使用json-server工具,首先我们需要创建一个db.json文件,其中将包含服务器的完整数据库。json-server工具将允许您执行以下操作:

  • GET请求,用于从文件中获取数据

  • POST请求,用于将新数据插入文件中

  • PUTPATCH请求,用于调整现有数据

  • 删除请求,用于删除数据

对于所有修改操作(POSTPUTPATCHDELETE),更新后的文件将由工具自动保存。

我们可以使用我们为帖子定义的默认状态作为帖子减速器的现有结构。但是,我们需要确保提供一个id值,以便稍后可以查询数据库:

[ { **"id": "react-hooks",** "title": "React Hooks", "content":  "The greatest thing since sliced bread!", "author":  "Daniel Bugl"  }, { **"id": "react-fragments",** "title":  "Using React Fragments", "content":  "Keeping the DOM tree clean!", "author":  "Daniel Bugl"  } ]

至于用户,我们需要想出一种存储用户名和密码的方法。为简单起见,我们只是以明文形式存储密码(在生产环境中不要这样做!)。在这里,我们还需要提供一个id值:

[
    { "id": 1, "username": "Daniel Bugl", "password": "supersecure42" }
]

此外,我们将在我们的数据库中存储主题。为了调查是否正确地从我们的数据库中提取主题,我们现在将定义第三个主题。和往常一样,每个主题都需要一个id值:

[
    { "id": 1, "primaryColor": "deepskyblue", "secondaryColor": "coral" },
    { "id": 2, "primaryColor": "orchid", "secondaryColor": "mediumseagreen" },
    { "id": 3, "primaryColor": "darkslategray", "secondaryColor": "slategray" }
]

现在,我们只需要将这三个数组合并成一个单独的 JSON 对象,将帖子数组存储在posts键下,将用户数组存储在users键下,将主题数组存储在themes键下。

让我们开始创建用作后端服务器数据库的 JSON 文件:

  1. 在我们应用程序文件夹的根目录中创建一个新的server/目录。

  2. 创建一个server/db.json文件,其中包含以下内容。我们可以使用 Reducer Hook 中的现有状态。但是,由于这是一个数据库,我们需要为每个元素提供一个id值(用粗体标记):

{
    "posts": [ { **"id": "react-hooks",** "title": "React Hooks", "content":  "The greatest thing since sliced bread!", "author":  "Daniel Bugl"  }, { **"id": "react-fragments",** "title":  "Using React Fragments", "content":  "Keeping the DOM tree clean!", "author":  "Daniel Bugl"  }
 ],
    "users": [
        { "id": 1, "username": "Daniel Bugl", "password": "supersecure42" }
    ],
    "themes": [
        { "id": 1, "primaryColor": "deepskyblue", "secondaryColor": "coral" },
        { "id": 2, "primaryColor": "orchid", "secondaryColor": "mediumseagreen" },
        { "id": 3, "primaryColor": "darkslategray", "secondaryColor": "slategray" }
    ]
}

对于json-server工具,我们只需要一个 JSON 文件作为数据库,该工具将为我们创建一个完整的 REST API。

安装 json-server 工具

现在,我们将通过使用json-server工具安装并启动我们的后端服务器:

  1. 首先,我们将通过npm安装json-server工具:
> npm install --save json-server
  1. 现在,我们可以通过调用以下命令启动我们的后端服务器:
>npx json-server --watch server/db.json

npx命令执行在项目中本地安装的命令。我们需要在这里使用npx,因为我们没有全局安装json-server工具(通过npm install -g json-server)。

我们执行了json-server工具,并让它监视我们之前创建的server/db.json文件。--watch标志意味着它将监听文件的更改,并自动刷新。

现在,我们可以转到http://localhost:3000/posts/react-hooks来查看我们的帖子对象:

我们简单的 JSON 服务器正在工作并提供帖子!

正如我们所看到的,这个工具为我们从数据库 JSON 文件创建了一个完整的 REST API!

配置 package.json

接下来,我们需要调整我们的package.json文件,以便启动服务器,以及我们的客户端(通过webpack-dev-server运行)。

让我们开始调整package.json文件:

  1. 首先,我们通过将其插入到package.json文件的scripts部分中来创建一个名为start:server的新包脚本。我们还确保更改端口,以便它不在与我们的客户端相同的端口上运行:
    "scripts": {
        "start:server": "npx json-server --watch server/db.json --port 4000",
        "start": "react-scripts start",
  1. 然后,我们将start脚本重命名为start:client
    "scripts": {
        "start:server": "npx json-server --watch server/db.json",
        "start:client": "react-scripts start",
  1. 接下来,我们安装一个名为concurrently的工具,它可以让我们同时启动服务器和客户端:
> npm install --save concurrently
  1. 现在,我们可以使用concurrently命令定义一个新的start脚本,然后将服务器和客户端命令作为参数传递给它:
    "scripts": {
 "start": "npx concurrently \"npm run start:server\" \"npm run start:client\"",

现在,运行npm start将运行客户端,以及后端服务器。

配置代理

最后,我们必须定义一个代理,以确保我们可以从与客户端相同的统一资源定位符(URL)请求我们的 API。这是必需的,否则,我们将不得不处理跨站点请求,这更加复杂。我们将定义一个代理,将从http://localhost:3000/api/转发请求到http://localhost:4000/

现在,让我们配置代理:

  1. 首先,我们必须安装http-proxy-middleware包:
> npm install --save http-proxy-middleware
  1. 然后,我们创建一个新的src/setupProxy.js文件,内容如下:
const proxy = require('http-proxy-middleware')

module.exports = function (app) {
    app.use(proxy('/api', {
  1. 接下来,我们必须定义代理的目标,即后端服务器,运行在http://localhost:4000上:
        target: 'http://localhost:4000',
  1. 最后,我们必须定义一个路径重写规则,它在转发请求到我们的服务器之前移除了/api前缀:
        pathRewrite: { '^/api': '' }
    }))
}

前面的代理配置将/api链接到我们的后端服务器;因此,我们现在可以通过以下命令同时启动服务器和客户端:

> npm start

然后,我们可以通过打开http://localhost:3000/api/posts/react-hooks来访问 API!

定义路由

默认情况下,json-server工具定义了以下路由:github.com/typicode/json-server#routes

我们还可以通过创建一个routes.json文件来定义自己的路由,我们可以将现有路由重写为其他路由:github.com/typicode/json-server#add-custom-routes

对于我们的博客应用程序,我们将定义一个单独的自定义路由:/login/:username/:password。我们将把这个路由链接到一个/users?username=:username&password=:password查询,以便找到具有给定用户名和密码组合的用户。

现在我们将为我们的应用程序定义自定义登录路由:

  1. 创建一个新的server/routes.json文件,内容如下:
{
    "/login/:username/:password": "/users?username=:username&password=:password"
}
  1. 然后,调整package.json文件中的start:server脚本,并添加--routes选项,如下所示:
        "start:server": "npx json-server --watch server/db.json --port 4000 --routes server/routes.json",

现在,我们的服务器将为我们提供自定义登录路由,我们将在本章后面使用它!我们可以尝试通过在浏览器中打开以下 URL 来登录:http://localhost:3000/api/login/Daniel%20Bugl/supersecure42。这将返回一个用户对象;因此,登录成功了!

我们可以在浏览器中看到用户对象以文本形式返回:

直接在浏览器中访问我们的自定义路由

正如我们所看到的,访问我们的自定义路由是有效的!我们现在可以使用它来登录用户。

示例代码

示例代码可以在Chapter06/chapter6_1文件夹中找到。

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

使用 Effect 和 State/Reducer Hooks 实现请求

在我们使用库来使用 Hooks 实现请求之前,我们将手动实现它们,使用 Effect Hook 来触发请求,使用 State/Reducer Hooks 来存储结果。

使用 Effect 和 State Hooks 进行请求

首先,我们将从服务器请求主题,而不是硬编码主题列表。

让我们使用 Effect Hook 和 State Hook 来实现请求主题:

  1. src/ChangeTheme.js文件中,调整 React 的import语句,以便导入useEffectuseState Hooks:
import React, { useEffect, useState } from 'react'
  1. 删除THEMES常量,即以下所有代码:
const  THEMES  = [ { primaryColor:  'deepskyblue', secondaryColor:  'coral'  }, { primaryColor:  'orchid', secondaryColor:  'mediumseagreen'  } ] 
  1. ChangeTheme组件中,定义一个新的useState Hook 来存储主题:
export default function ChangeTheme ({ theme, setTheme }) {
 const [ themes, setThemes ] = useState([])
  1. 然后定义一个useEffect Hook,我们将在其中进行请求:
    useEffect(() => {
  1. 在这个 Hook 中,我们使用fetch来请求一个资源;在这种情况下,我们请求/api/themes
        fetch('/api/themes')
  1. Fetch 利用了 Promise API;因此,我们可以使用.then()来处理结果。首先,我们必须将结果解析为 JSON:
            .then(result => result.json())
  1. 最后,我们使用来自我们请求的主题数组调用setThemes
            .then(themes => setThemes(themes))

我们还可以将前面的函数缩短为.then(setThemes),因为我们只传递了从.then()中获取的themes参数。

  1. 目前,这个 Effect Hook 应该只在组件挂载时触发,所以我们将空数组作为第二个参数传递给useEffect。这确保了 Effect Hook 没有依赖项,因此只会在组件挂载时触发:
    }, [])
  1. 现在,唯一剩下的事情就是用我们从 Hook 中获取的themes值替换THEMES常量:
            {themes.map(t =>

正如我们所看到的,现在有三个主题可用,都是通过我们的服务器从数据库加载的:

使用 Hooks 从我们的服务器加载了三个主题!

我们的主题现在是从后端服务器加载的,我们可以继续通过 Hooks 请求帖子。

使用 Effect 和 Reducer Hooks 进行请求

现在,我们将使用后端服务器来请求帖子数组,而不是将其硬编码为postsReducer的默认值。

让我们使用 Effect Hook 和 Reducer Hook 来请求帖子:

  1. 删除src/App.js中的defaultPosts常量定义,即以下所有代码:
const  defaultPosts  = [ { title:  'React Hooks', content:  'The greatest thing since sliced bread!', author:  'Daniel Bugl'  }, { title:  'Using React Fragments', content:  'Keeping the DOM tree clean!', author:  'Daniel Bugl'  } ]
  1. 用一个空数组替换useReducer函数中的defaultPosts常量:
 const  [  state,  dispatch  ]  =  useReducer(appReducer,  { user:  '', posts:  []  })
  1. src/reducers.js中,在postsReducer函数中定义一个新的动作类型,称为FETCH_POSTS。这个动作类型将用新的帖子数组替换当前状态:
function postsReducer (state, action) {
    switch (action.type) {
 case 'FETCH_POSTS':
 return action.posts
  1. src/App.js中,定义一个新的useEffect Hook,位于当前的 Hook 之前:
    useEffect(() => {
  1. 在这个 Hook 中,我们再次使用fetch来请求一个资源;在这种情况下,我们请求/api/posts
        fetch('/api/posts')
            .then(result => result.json())
  1. 最后,我们使用来自我们请求的posts数组分派了一个FETCH_POSTS动作:
            .then(posts => dispatch({ type: 'FETCH_POSTS', posts }))
  1. 目前,这个 Effect Hook 应该只在组件挂载时触发,所以我们将空数组作为第二个参数传递给useEffect
    }, [])

正如我们所看到的,现在帖子是从服务器请求的!我们可以查看 DevTools Network 标签以查看请求:

从我们的服务器请求帖子!

现在从后端服务器请求帖子。在下一节中,我们将使用axiosreact-request-hook从服务器请求资源。

示例代码

示例代码可以在Chapter06/chapter6_2文件夹中找到。

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

使用 axios 和 react-request-hook

在上一节中,我们使用 Effect Hook 触发请求,并使用 Reducer/State Hook 从请求的结果更新状态。与手动实现请求不同,我们可以使用axiosreact-request-hook库轻松地使用 Hooks 来实现请求。

设置这些库

在我们开始使用axiosreact-request-hook之前,我们必须设置一个axios实例和一个RequestProvider组件。

让我们开始设置这些库:

  1. 首先,我们安装这些库:
>npm install --save react-request-hook axios
  1. 然后,在src/index.js中导入它们:
import { RequestProvider } from 'react-request-hook'
import axios from 'axios'
  1. 现在,我们定义一个axios实例,其中我们将baseURL设置为http://localhost:3000/api/—我们的后端服务器:
const axiosInstance = axios.create({
    baseURL: 'http://localhost:3000/api/'
})

在我们的axios实例配置中,我们还可以定义其他选项,例如请求的默认超时时间或自定义标头。有关更多信息,请查看axios文档:github.com/axios/axios#axioscreateconfig

  1. 最后,我们用<RequestProvider>组件包装我们的<App />组件。删除以下代码行:
ReactDOM.render(<App />, document.getElementById('root'));

用以下代码替换它:

ReactDOM.render(
    <RequestProvider value={axiosInstance}>
        <App />
    </RequestProvider>,
    document.getElementById('root')
)

现在,我们的应用程序已准备好使用 Resource Hooks!

使用 useResource Hook

处理请求的更强大的方法是使用axiosreact-request-hook库。使用这些库,我们可以访问可以取消单个请求甚至清除所有待处理请求的功能。此外,使用这些库可以更容易地处理错误和加载状态。

现在我们将实现useResource Hook 以从服务器请求主题:

  1. src/ChangeTheme.js中,从react-request-hook库中导入useResource Hook:
import { useResource } from 'react-request-hook'
  1. 删除先前定义的 State 和 Effect Hooks。

  2. 然后,在ChangeTheme组件中定义一个useResource Hook。该 Hook 返回一个值和一个 getter 函数。调用 getter 函数将请求资源:

export default function ChangeTheme ({ theme, setTheme }) {
 const [ themes, getThemes ] = useResource(() => ({

在这里,我们使用了() => { return { } }的简写语法,即() => ({ })。使用这种简写语法可以简洁地编写只返回对象的函数。

  1. 在这个 Hook 中,我们传递一个函数,该函数返回有关请求的信息的对象:
        url: '/themes',
        method: 'get'
    }))

使用axios,我们只需要将/themes作为url传递,因为我们已经定义了包含/api/baseURL

  1. Resource Hook 返回一个带有data值、isLoading布尔值、error对象和cancel函数的对象,用于取消挂起的请求。现在,我们从themes对象中提取出data值和isLoading布尔值:
    const { data, isLoading } = themes
  1. 然后,我们定义一个useEffect Hook 来触发getThemes函数。我们只希望它在组件挂载时触发一次,因此我们将空数组作为第二个参数传递:
    useEffect(getThemes, [])
  1. 此外,我们使用isLoading标志在等待服务器响应时显示加载消息:
            {isLoading && ' Loading themes...'}
  1. 最后,我们将themes值重命名为从useResource Hook 返回的data值,并添加条件检查以确保data值已经可用:
            {data && data.map(t =>

如果我们现在看一下我们的应用程序,我们会发现“加载主题…”的消息会在很短的时间内显示,然后从我们的数据库中显示主题!现在我们可以继续使用 Resource Hook 请求帖子。

使用 Reducer Hook 与 useResource

useResource Hook 已经处理了我们请求结果的状态,所以我们不需要额外的useState Hook 来存储状态。然而,如果我们已经有一个现有的 Reducer Hook,我们可以将其与useResource Hook 结合使用。

现在我们将在我们的应用程序中实现useResource Hook 与 Reducer Hook 的组合使用:

  1. src/App.js中,从react-request-hook库中导入useResource Hook:
import { useResource } from 'react-request-hook'
  1. 删除先前使用fetch请求/api/postsuseEffect Hook。

  2. 定义一个新的useResource Hook,在其中请求/posts

    const [ posts, getPosts ] = useResource(() => ({
        url: '/posts',
        method: 'get'
    }))
  1. 定义一个新的useEffect Hook,简单地调用getPosts
    useEffect(getPosts, [])
  1. 最后,定义一个useEffect Hook,在检查数据是否已经存在后,触发FETCH_POSTS动作:
    useEffect(() => {
        if (posts && posts.data) {
            dispatch({ type: 'FETCH_POSTS', posts: posts.data })
        }
  1. 我们确保这个 Effect Hook 在posts对象更新时触发:
    }, [posts])

现在,当我们获取新的帖子时,将会触发FETCH_POSTS动作。接下来,我们将处理请求期间的错误。

处理错误状态

我们已经在ChangeTheme组件中处理了加载状态。现在,我们将实现帖子的错误状态。

让我们开始处理帖子的错误状态:

  1. src/reducers.js中,使用新的动作类型POSTS_ERROR定义一个新的errorReducer函数:
function errorReducer (state, action) {
    switch (action.type) {
        case 'POSTS_ERROR':
            return 'Failed to fetch posts'

        default:
            return state
    }
}
  1. errorReducer函数添加到我们的appReducer函数中:
export default function appReducer (state, action) {
    return {
        user: userReducer(state.user, action),
        posts: postsReducer(state.posts, action),
 error: errorReducer(state.error, action)
    }
}
  1. src/App.js中,调整我们的 Reducer Hook 的默认状态:
    const [ state, dispatch ] = useReducer(appReducer, { user: '', posts: [], error: '' })
  1. state对象中取出error值:
    const { user, error } = state
  1. 现在,我们可以调整处理来自posts资源的新数据的现有 Effect Hook,在出现错误的情况下分派POSTS_ERROR动作:
    useEffect(() => {
 if (posts && posts.error) {
 dispatch({ type: 'POSTS_ERROR' })
 }
        if (posts && posts.data) {
            dispatch({ type: 'FETCH_POSTS', posts: posts.data })
        }
    }, [posts])
  1. 最后,在PostList组件之前显示错误消息:
 {error && <b>{error}</b>}
                 <PostList />

如果现在只启动客户端(通过npm run start:client),将显示错误:

在请求失败时显示错误!

如我们所见,由于服务器未运行,我们的应用程序中显示了无法获取帖子的错误。现在我们可以继续通过请求实现帖子创建。

实现帖子创建

现在我们已经很好地掌握了如何从 API 请求数据,我们将使用useResource Hook 来创建新数据。

让我们开始使用 Resource Hook 实现帖子创建:

  1. 编辑src/post/CreatePost.js,并导入useResource Hook:
import { useResource } from 'react-request-hook'
  1. 然后,在其他 Hooks 之后但在处理程序函数定义之前,定义一个新的 Resource Hook。在这里,我们将方法设置为post(创建新数据),并将数据从createPost函数传递给请求配置:
    const [ , createPost ] = useResource(({ title, content, author }) => ({
        url: '/posts',
        method: 'post',
        data: { title, content, author }
    }))

在这里,我们使用了数组解构的简写语法:我们通过不指定值名称来忽略数组的第一个元素。我们不写const [ post, createPost ],然后不使用post,而是直接写一个逗号,如下所示:const [  , createPost ]

  1. 现在,我们可以在handleCreate处理程序函数中使用createPost函数。我们确保保留对dispatch函数的调用,以便在等待服务器响应的同时立即在客户端插入新帖子。添加的代码已用粗体标出:
    function handleCreate () {
 createPost({ title, content, author: user })
        dispatch({ type: 'CREATE_POST', title, content, author: user })
    }

请注意,在这个简单的例子中,我们不期望或处理帖子创建的失败。在这种情况下,我们甚至在请求完成之前就分派了动作。然而,在实施登录时,我们将处理来自请求的错误状态,以检查用户是否成功登录。在真实世界的应用程序中,始终处理错误状态是最佳实践。

  1. 请注意,现在插入帖子时,帖子将首先出现在列表的开头;然而,刷新后,它将出现在列表的末尾。不幸的是,我们的服务器将新帖子插入到列表的末尾。因此,在从服务器获取帖子后,我们将颠倒顺序。编辑src/App.js,并调整以下代码:
        if (posts && posts.data) {
            dispatch({ type: 'FETCH_POSTS', posts: posts.data.reverse() })
        }

现在,通过服务器插入新帖子运行良好,我们可以继续实施注册!

实施注册

接下来,我们将实施注册,这将与创建帖子的方式非常相似。

让我们开始实施注册:

  1. 首先,在src/user/Register.js中导入useEffectuseResource钩子:
import React, { useState, useContext, useEffect } from 'react'
import { useResource } from 'react-request-hook'
  1. 然后,在其他钩子下面和处理程序函数之前定义一个新的useResource钩子。与我们在创建帖子时所做的不同,我们现在还希望存储生成的user对象:
    const [ user, register ] = useResource((username, password) => ({
        url: '/users',
        method: 'post',
        data: { username, password }
    }))
  1. 接下来,在useResource钩子下面定义一个新的useEffect钩子,当请求完成时将分派一个REGISTER动作:
    useEffect(() => {
        if (user && user.data) {
            dispatch({ type: 'REGISTER', username: user.data.username })
        }
    }, [user])

请注意,在这个简单的例子中,我们不期望或处理注册的失败。在这种情况下,我们只在用户成功创建后分派动作。然而,在实施登录时,我们将处理来自请求的错误状态,以检查用户是否成功登录。在真实世界的应用程序中,始终处理错误状态是最佳实践。

  1. 最后,我们调整表单提交处理程序,以调用register函数,而不是直接分派动作:
        <form onSubmit={e => { e.preventDefault(); register(username, password) }}>

现在,如果我们输入用户名和密码,然后点击注册,一个新用户将被插入到我们的db.json文件中,就像以前一样,我们将登录。我们现在继续通过资源钩子实施登录。

实施登录

最后,我们将通过使用我们的自定义路由来实施登录。这样做后,我们的博客应用程序将完全连接到服务器。

让我们开始实施登录:

  1. 首先,编辑src/user/Login.js并导入useEffectuseResource钩子:
import React, { useState, useContext, useEffect } from 'react'
import { useResource } from 'react-request-hook'
  1. 我们定义了一个新的 State Hook,它将存储一个布尔值,用于检查登录是否失败:
    const [ loginFailed, setLoginFailed ] = useState(false)
  1. 然后,我们为密码字段定义一个新的 State Hook,因为之前我们没有处理它:
    const [ password, setPassword ] = useState('')
  1. 现在,我们为密码字段定义一个处理函数,放在handleUsername函数下面:
    function handlePassword (evt) {
        setPassword(evt.target.value)
    }
  1. 接下来,我们处理input字段中的值更改:
            <input type="password" value={password} onChange={handlePassword} name="login-username" id="login-username" />
  1. 现在,我们可以在 State Hooks 下面定义我们的 Resource Hook,在这里我们将usernamepassword传递给/login路由。由于我们将它们作为 URL 的一部分传递,我们需要确保首先正确地对它们进行编码:
    const [ user, login ] = useResource((username, password) => ({
        url: `/login/${encodeURI(username)}/${encodeURI(password)}`,
        method: 'get'
    }))

请注意,通过 GET 请求以明文发送密码是不安全的。我们之所以这样做,只是为了简化配置我们的虚拟服务器。在真实世界的应用程序中,应该使用 POST 请求进行登录,并将密码作为 POST 数据的一部分发送。还要确保使用超文本传输安全协议(HTTPS),以便对 POST 数据进行加密。

  1. 接下来,我们定义一个 Effect Hook,如果请求成功完成,它将分派LOGIN动作:
    useEffect(() => {
        if (user && user.data) {
  1. 因为登录路由返回的要么是一个空数组(登录失败),要么是一个包含单个用户的数组,所以我们需要检查数组是否至少包含一个元素:
            if (user.data.length > 0) {
                setLoginFailed(false)
                dispatch({ type: 'LOGIN', username: user.data[0].username })
            } else {
  1. 如果数组为空,我们将loginFailed设置为true
                setLoginFailed(true)
            }
        }
  1. 如果我们从服务器获得错误响应,我们还将登录状态设置为失败:
        if (user && user.error) {
            setLoginFailed(true)
        }
  1. 我们确保 Effect Hook 在 Resource Hook 更新user对象时触发:
    }, [user])
  1. 然后,我们调整formonSubmit函数,以调用login函数:
 <form onSubmit={e => { e.preventDefault(); login(username, password**)** }}>
  1. 最后,在提交按钮下面,我们显示“用户名或密码无效”的消息,以防loginFailed被设置为true
            {loginFailed && <span style={{ color: 'red' }}>Invalid username or password</span>}

正如我们所看到的,输入错误的用户名或密码(或没有密码)将导致错误,而输入正确的用户名/密码组合将使我们登录:

在登录失败时显示错误消息

现在,我们的应用程序已完全连接到后端服务器!

示例代码

示例代码可以在Chapter06/chapter6_3文件夹中找到。

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

使用 React.memo 防止不必要的重新渲染

在类组件中,我们有shouldComponentUpdate,它可以防止组件在 props 没有改变时重新渲染。

使用函数组件,我们可以使用React.memo来做同样的事情,这是一个高阶组件。React.memo会记住上次渲染的结果,如果 props 没有改变,它将跳过重新渲染组件:

const SomeComponent = () => ...

export default React.memo(SomeComponent)

默认情况下,React.memo将像shouldComponentUpdate的默认定义一样,它只会浅层比较 props 对象。如果我们想要进行特殊比较,可以将函数作为第二个参数传递给React.memo

export default React.memo(SomeComponent, (prevProps, nextProps) => {
    // compare props and return true if the props are equal and we should not update
})

shouldComponentUpdate不同,传递给React.memo的函数在 props 相等时返回true,因此它不应该更新,这与shouldComponentUpdate的工作方式相反!学习了React.memo之后,让我们尝试在实践中实现React.memo来为 Post 组件。

在 Post 组件中实现 React.memo

首先,让我们找出Post组件何时重新渲染。为此,我们将向Post组件添加一个console.log语句,如下所示:

  1. 编辑src/post/Post.js,并在组件渲染时添加以下调试输出:
export default function Post ({ title, content, author }) {
 console.log('rendering Post')
  1. 现在,打开http://localhost:3000的应用程序,并打开 DevTools(在大多数浏览器上:右键单击|在页面上检查)。转到控制台选项卡,您应该看到输出两次,因为我们正在渲染两篇文章:

渲染两篇文章时的调试输出

  1. 到目前为止,一切顺利。现在,让我们尝试登录,并看看会发生什么:

登录后重新渲染文章

正如我们所看到的,登录后,文章组件不必要地重新渲染,尽管它们的 props 没有改变。我们可以使用React.memo来防止这种情况,如下所示:

  1. 编辑src/post/Post.js,并删除函数定义的 export default 部分(用粗体标记):
export default function Post ({ title, content, author }) {
  1. 然后,在文件底部,将 Post 组件包装在React.memo()中后导出:
export default React.memo(Post)
  1. 现在,刷新页面并重新登录。我们可以看到两篇文章被渲染,这产生了初始的调试输出。然而,现在登录不再导致文章组件重新渲染了!

如果我们想要对文章是否相等进行自定义检查,我们可以比较titlecontentauthor,如下所示:

export  default  React.memo(Post, (prev,  next)  =>  prev.title ===  next.title && prev.content === next.content && prev.author === next.author ) 

在我们的情况下,这样做将产生相同的效果,因为 React 默认已经对所有 props 进行了浅比较。当我们有深层对象需要比较,或者当我们想要忽略某些 props 的更改时,这个函数才会变得有用。请注意,我们不应该过早地优化我们的代码。重新渲染可能没问题,因为 React 是智能的,如果没有变化,它就不会在浏览器中绘制。因此,除非已经确定某种情况是性能瓶颈,否则优化所有重新渲染可能有些过度。

示例代码

示例代码可以在Chapter06/chapter6_4文件夹中找到。

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

使用 React Suspense 实现懒加载

React Suspense 允许我们在渲染之前让组件等待。目前,React Suspense 只允许我们使用React.lazy动态加载组件。将来,Suspense 将支持其他用例,比如数据获取。

React.lazy是另一种性能优化的形式。它让我们动态加载组件以减少捆绑包大小。有时我们希望在初始渲染时避免加载所有组件,只在需要时请求特定组件。

例如,如果我们的博客有一个会员区域,我们只需要在用户登录后加载它。这样做将减少那些只访问我们博客阅读博文的访客的捆绑包大小。为了了解 React Suspense,我们将在我们的博客应用程序中懒加载Logout组件。

实现 React.Suspense

首先,我们必须指定一个加载指示器,在我们的懒加载组件加载时显示。在我们的示例中,我们将使用 React Suspense 包装UserBar组件。

编辑src/App.js,并用以下代码替换<UserBar />组件:

                    <React.Suspense fallback={"Loading..."}>
                        <UserBar />
                    </React.Suspense>

现在,我们的应用程序已准备好实现懒加载。

实现 React.lazy

接下来,我们将通过使用React.lazy()来实现Logout组件的懒加载:

  1. 编辑 src/user/UserBar.js,并删除Logout组件的导入语句:
import Logout from './Logout'
  1. 然后,通过懒加载定义Logout组件:
const Logout = React.lazy(() => import('./Logout'))

import()函数动态加载Logout组件从Logout.js文件中。与静态的import语句相反,这个函数只有在React.lazy触发时才会被调用,这意味着只有在需要组件时才会被导入。

如果我们想看到延迟加载的效果,可以在 Google Chrome 中将网络节流设置为 Slow 3G:

在 Google Chrome 中将网络节流设置为 Slow 3G,在 Firefox 中,我们可以通过将网络节流设置为 GPRS 来实现相同的效果。

不幸的是,Safari 目前还没有这样的功能,但我们可以使用苹果的“硬件 IO 工具”中的 Network Link Conditioner 工具:developer.apple.com/download/more/

如果我们现在刷新页面,然后登录,我们首先可以看到“加载中…”的消息,然后会显示Logout组件。如果我们查看网络日志,我们可以看到Logout组件是通过网络请求的:

通过网络加载的注销组件

正如我们所看到的,Logout组件现在是懒加载的,这意味着只有在需要时才会被请求。

示例代码

示例代码可以在Chapter06/chapter6_5文件夹中找到。

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

总结

在本章中,我们首先学习了如何从 JSON 文件设置 API 服务器。然后,我们学习了如何使用 Effect 和 State/Reducer Hooks 请求资源。接下来,我们学习了如何使用axiosreact-request-hook库请求资源。最后,我们学习了如何使用React.memo来防止不必要的重新渲染,以及如何使用 React Suspense 来懒加载组件。

在下一章中,我们将为我们的应用程序添加路由,并学习如何使用 Hooks 进行路由。

问题

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

  1. 我们如何可以轻松地从简单的 JSON 文件创建一个完整的 REST API?

  2. 使用代理访问后端服务器在开发过程中有哪些优势?

  3. 我们可以使用哪些 Hooks 组合来实现请求?

  4. 我们可以使用哪些库来实现请求?

  5. 我们如何使用react-request-hook处理加载状态?

  6. 我们如何使用react-request-hook处理错误?

  7. 我们如何防止组件不必要的重新渲染?

  8. 我们如何减少应用程序的捆绑大小?

进一步阅读

如果您对我们在本章中探讨的概念更感兴趣,可以查阅以下阅读材料:

第七章:使用 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. 我们如何测试异步代码?

进一步阅读

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

第三部分:集成和迁移

在书的最后部分,我们将学习如何将现有的状态管理解决方案与 Hooks 结合使用。此外,我们将演示如何将 React 类组件以及现有的 Redux 和 MobX 应用程序迁移到 Hooks。

在本节中,我们将涵盖以下章节:

  • 第十一章,从 React 类组件迁移

  • 第十二章,Redux 和 Hooks

  • 第十三章,MobX 和 Hooks

第十一章:从 React 类组件迁移

在上一章中,我们学习了如何通过从现有代码中提取自定义 Hooks 来构建我们自己的 Hooks。然后,我们在博客应用程序中使用了我们自己的 Hooks,并学习了本地 Hooks 和 Hooks 之间的交互。最后,我们学习了如何使用 React Hooks 测试库为 Hooks 编写测试,并为我们的自定义 Hooks 实现了测试。

在本章中,我们将首先使用 React 类组件实现一个待办事项应用程序。接下来,我们将学习如何将现有的 React 类组件应用程序迁移到 Hooks。在实践中看到使用 Hooks 的函数组件和类组件之间的差异将加深我们对使用任一解决方案的权衡的理解。此外,到本章结束时,我们将能够将现有的 React 应用程序迁移到 Hooks。

本章将涵盖以下主题:

  • 使用类组件处理状态

  • 从类组件迁移应用程序到 Hooks

  • 了解类组件与 Hooks 的权衡

技术要求

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

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

观看以下视频以查看代码的实际操作:

bit.ly/2Mm9yoC

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

现在,让我们开始本章。

使用类组件处理状态

在我们开始从类组件迁移到 Hooks 之前,我们将使用 React 类组件创建一个小型的待办事项列表应用程序。在下一节中,我们将使用 Hooks 将这些类组件转换为函数组件。最后,我们将比较这两种解决方案。

设计应用程序结构

与我们之前在博客应用程序中所做的一样,我们将首先考虑我们应用程序的基本结构。对于这个应用程序,我们将需要以下功能:

  • 一个标题

  • 添加新待办事项的方法

  • 以列表的方式显示所有待办事项

  • 待办事项的筛选器

从模拟开始总是一个好主意。所以,让我们开始吧:

  1. 我们首先绘制一个 ToDo 应用程序界面的模拟:

我们的 ToDo 应用程序的模拟

  1. 接下来,我们以类似的方式定义基本组件,就像我们在博客应用程序中所做的那样:

在我们应用程序的模拟中定义基本组件

  1. 现在我们可以定义容器组件:

在我们应用程序的模拟中定义容器组件

正如我们所看到的,我们将需要以下组件:

  • App

  • 标题

  • 添加待办事项

  • TodoList

  • TodoItem

  • TodoFilter(+ TodoFilterItem)

TodoList组件使用TodoItem组件,用于显示一个带有复选框完成和删除按钮的项目。TodoFilter组件内部使用TodoFilterItem组件来显示各种筛选器。

初始化项目

我们将使用create-react-app来创建一个新项目。让我们现在初始化项目:

  1. 运行以下命令:
> npx create-react-app chapter11_1
  1. 然后,删除src/App.css,因为我们不需要它。

  2. 接下来,编辑src/index.css,并调整边距如下:

    margin: 20px;
  1. 最后,删除当前的src/App.js文件,因为我们将在下一步创建一个新的文件。

现在,我们的项目已经初始化,我们可以开始定义应用程序结构。

定义应用程序结构

我们已经从模拟中知道了我们的应用程序的基本结构,所以让我们从定义App组件开始:

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

  2. 导入ReactHeaderAddTodoTodoListTodoFilter组件:

import React from 'react'

import Header from './Header'
import AddTodo from './AddTodo'
import TodoList from './TodoList'
import TodoFilter from './TodoFilter'
  1. 现在将App组件定义为类组件。现在,我们只会定义render方法:
export default class App extends React.Component {
    render () {
        return (
            <div style={{ width: 400 }}>
                <Header />
                <AddTodo />
                <hr />
                <TodoList />
                <hr />
                <TodoFilter />
            </div>
        )
    }
}

App组件定义了我们应用程序的基本结构。它将包括一个标题,一种添加新待办事项的方法,待办事项列表和一个筛选器。

定义组件

现在,我们将定义组件作为静态组件。在本章的后面,我们将为它们实现动态功能。现在,我们将实现以下静态组件:

  • 标题

  • 添加待办事项

  • TodoList

  • TodoItem

  • TodoFilter

现在让我们开始实现这些组件。

定义标题组件

我们将从Header组件开始,因为它是所有组件中最简单的:

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

  2. 导入React并定义带有render方法的类组件:

import React from 'react'

export default class Header extends React.Component {
    render () {
        return <h1>ToDo</h1>
    }
}

现在,我们已经定义了应用程序的Header组件。

定义 AddTodo 组件

接下来,我们将定义AddTodo组件,它渲染一个input字段和一个按钮。

现在让我们实现AddTodo组件:

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

  2. 导入React并定义类组件和render方法:

import React from 'react'

export default class AddTodo extends React.Component {
    render () {
        return (
  1. render方法中,我们返回一个包含input字段和添加按钮的form
            <form>
                <input type="text" placeholder="enter new task..." style={{ width: 350, height: 15 }} />
                <input type="submit" style={{ float: 'right', marginTop: 2 }} value="add" />
            </form>
        )
    }
}

正如我们所看到的,AddTodo组件由一个input字段和一个按钮组成。

定义 TodoList 组件

现在,我们定义了TodoList组件,它将使用TodoItem组件。目前,我们将在这个组件中静态地定义两个待办事项。

让我们开始定义TodoList组件:

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

  2. 导入ReactTodoItem组件:

import React from 'react'

import TodoItem from './TodoItem'
  1. 然后,定义类组件和render方法:
export default class TodoList extends React.Component {
    render () {
  1. 在这个render方法中,我们静态地定义了两个待办事项:
        const items = [
            { id: 1, title: 'Write React Hooks book', completed: true },
            { id: 2, title: 'Promote book', completed: false }
        ]
  1. 最后,我们将使用map函数来渲染项目:
        return items.map(item =>
            <TodoItem {...item} key={item.id} />
        )
    }
}

正如我们所看到的,TodoList组件渲染了一系列TodoItem组件。

定义 TodoItem 组件

在定义了TodoList组件之后,我们现在将定义TodoItem组件,以便渲染单个项目。

让我们开始定义TodoItem组件:

  1. 创建一个新的src/TodoItem.js组件。

  2. 导入React,并定义组件,以及render方法:

import React from 'react'

export default class TodoItem extends React.Component {
    render () {
  1. 现在,我们将使用解构来获取titlecompleted属性:
        const { title, completed } = this.props
  1. 最后,我们将渲染一个包含checkboxtitlebuttondiv元素来删除项目:
        return (
            <div style={{ width: 400, height: 25 }}>
                <input type="checkbox" checked={completed} />
                {title}
                <button style={{ float: 'right' }}>x</button>
            </div>
        )
    }
}

TodoItem组件由一个复选框、title和一个删除项目的button组成。

定义 TodoFilter 组件

最后,我们将定义TodoFilter组件。在同一个文件中,我们将为TodoFilterItem定义另一个组件。

让我们开始定义TodoFilterItemTodoFilter组件:

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

  2. TodoFilterItem定义一个类组件:

class TodoFilterItem extends React.Component {
    render () {
  1. 在这个render方法中,我们使用解构来获取name属性:
        const { name } = this.props
  1. 接下来,我们将为style定义一个对象:
        const style = {
            color: 'blue',
            cursor: 'pointer'
        }
  1. 然后,我们返回一个带有过滤器name值的span元素,并使用定义的style对象:
        return <span style={style}>{name}</span>
    }
}
  1. 最后,我们可以定义实际的TodoFilter组件,它将呈现三个TodoFilterItem组件,如下所示:
export default class TodoFilter extends React.Component {
    render () {
        return (
            <div>
                <TodoFilterItem name="all" />{' / '}
                <TodoFilterItem name="active" />{' / '}
                <TodoFilterItem name="completed" />
            </div>
        )
    }
}

现在,我们有一个列出三种不同过滤可能性的组件:allactivecompleted

实现动态代码

现在我们已经定义了所有静态组件,我们的应用程序应该看起来像模拟一样。下一步是使用 React 状态、生命周期和处理程序方法实现动态代码。

在本节中,我们将执行以下操作:

  • 定义模拟 API

  • 定义一个StateContext

  • 使App组件动态化

  • 使AddTodo组件动态化

  • 使TodoList组件动态化

  • 使TodoItem组件动态化

  • 使TodoFilter组件动态化

让我们开始。

定义 API 代码

首先,我们将定义一个 API,用于获取待办事项。在我们的情况下,我们将简单地在短暂延迟后返回一个待办事项数组。

让我们开始实现模拟 API:

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

  2. 我们将定义一个函数,根据通用唯一标识符UUID)函数生成我们待办事项的随机 ID:

export const generateID = () => {
    const S4 = () =>(((1+Math.random())*0x10000)|0).toString(16).substring(1)
    return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4())
}
  1. 然后,我们定义了fetchAPITodos函数,它返回一个Promise,在短暂延迟后解析:
export const fetchAPITodos = () =>
    new Promise((resolve) =>
        setTimeout(() => resolve([
            { id: generateID(), title: 'Write React Hooks book', completed: true },
            { id: generateID(), title: 'Promote book', completed: false }
        ]), 100)
    )

现在,我们有一个函数,模拟从 API 获取待办事项,通过在100毫秒延迟后返回一个数组。

定义 StateContext

接下来,我们将定义一个上下文,用于保存我们当前的待办事项列表。我们将称此上下文为StateContext

让我们现在开始实现StateContext

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

  2. 导入React,如下所示:

import React from 'react'
  1. 现在,定义StateContext并将空数组设置为回退值:
const StateContext = React.createContext([])
  1. 最后,导出StateContext
export default StateContext

现在,我们有一个上下文,可以在其中存储我们的待办事项数组。

使 App 组件动态化

现在,我们将通过添加功能来获取、添加、切换、过滤和删除待办事项,使App组件动态化。此外,我们将定义一个StateContext提供程序。

让我们开始使App组件动态化:

  1. src/App.js中,在其他导入语句之后导入StateContext
import StateContext from './StateContext'
  1. 然后,从src/api.js文件导入fetchAPITodosgenerateID函数:
import { fetchAPITodos, generateID } from './api'
  1. 接下来,我们将修改我们的App类代码,实现一个constructor,它将设置初始状态:
export default class App extends React.Component {
 constructor (props) {
  1. 在这个constructor中,我们需要首先调用super,以确保父类(React.Component)的构造函数被调用,并且组件得到正确初始化:
        super(props)
  1. 现在,我们可以通过设置this.state来设置初始状态。最初,没有待办事项,filter值将设置为'all'
        this.state = { todos: [], filteredTodos: [], filter: 'all' }
    }
  1. 然后,我们定义componentDidMount生命周期方法,该方法将在组件首次渲染时获取待办事项:
    componentDidMount () {
        this.fetchTodos()
    }
  1. 现在,我们将定义实际的fetchTodos方法,在我们的情况下,它只是设置状态,因为我们不打算将这个简单的应用程序连接到后端。我们还将调用this.filterTodos()来在获取待办事项后更新filteredTodos数组:
    fetchTodos () {
        fetchAPITodos().then((todos) => {
            this.setState({ todos })
            this.filterTodos()
        })
    }
  1. 接下来,我们定义addTodo方法,它创建一个新项目,并将其添加到状态数组中,类似于我们在博客应用中使用 Hooks 所做的操作:
    addTodo (title) {
        const { todos } = this.state

        const newTodo = { id: generateID(), title, completed: false }

        this.setState({ todos: [ newTodo, ...todos ] })
        this.filterTodos()
    }
  1. 然后,我们定义toggleTodo方法,该方法使用map函数来查找和修改特定的待办事项:
    toggleTodo (id) {
        const { todos } = this.state

        const newTodos = todos.map(t => {
            if (t.id === id) {
                return { ...t, completed: !t.completed }
            }
            return t
        }, [])

        this.setState({ todos: newTodos })
        this.filterTodos()
    }
  1. 现在,我们定义removeTodo方法,该方法使用filter函数来查找并删除特定的待办事项:
    removeTodo (id) {
        const { todos } = this.state

        const newTodos = todos.filter(t => {
            if (t.id === id) {
                return false
            }
             return true
        })

        this.setState({ todos: newTodos })
        this.filterTodos()
    }
  1. 然后,我们定义一个方法来对我们的待办事项应用特定的filter
    applyFilter (todos, filter) {
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }
  1. 现在,我们可以定义filterTodos方法,该方法将调用applyFilter方法,并更新filteredTodos数组和filter值:
    filterTodos (filterArg) {
        this.setState(({ todos, filter }) => ({
            filter: filterArg || filter,
            filteredTodos: this.applyFilter(todos, filterArg || filter)
        }))
    }

我们使用filterTodos来在添加/删除项目以及更改过滤器后重新过滤待办事项,为了使这两个功能都能正常工作,我们需要检查是否传递了filter参数filterArg。如果没有,我们将退回到state中的当前filter参数。

  1. 然后,我们调整render方法,以使用状态为StateContext提供一个值,并将某些方法传递给组件:
    render () {
 const { filter, filteredTodos } = this.state

        return (
 <StateContext.Provider value={filteredTodos}>
                <div style={{ width: 400 }}>
                    <Header />
                    <AddTodo addTodo={this.addTodo} />
                    <hr />
                    <TodoList toggleTodo={this.toggleTodo} removeTodo={this.removeTodo} />
                    <hr />
                    <TodoFilter filter={filter} filterTodos={this.filterTodos} />
                </div>
 </StateContext.Provider>
        )
    }
  1. 最后,我们需要重新绑定this到类,以便我们可以将方法传递给我们的组件,而不会改变this上下文。调整constructor如下:
            constructor () {
                super(props)

                this.state = { todos: [], filteredTodos: [], filter: 
                  'all' }

 this.fetchTodos = this.fetchTodos.bind(this)
 this.addTodo = this.addTodo.bind(this)
 this.toggleTodo = this.toggleTodo.bind(this)
 this.removeTodo = this.removeTodo.bind(this)
 this.filterTodos = this.filterTodos.bind(this)
            }

现在,我们的App组件可以动态地获取、添加、切换、删除和过滤待办事项。正如我们所看到的,当我们使用类组件时,我们需要重新绑定处理程序函数的this上下文到类。

使 AddTodo 组件动态化

在使我们的App组件动态化之后,现在是时候使我们的所有其他组件也动态化了。我们将从顶部开始,从AddTodo组件开始。

现在让AddTodo组件动态化:

  1. src/AddTodo.js中,我们首先定义了一个constructor,它为input字段设置了初始state
export default class AddTodo extends React.Component {
    constructor (props) {
        super(props)

        this.state = {
            input: ''
        }
    }
  1. 然后,我们定义一个处理input字段变化的方法:
    handleInput (e) {
        this.setState({ input: e.target.value })
    }
  1. 现在,我们将定义一个可以处理添加新待办事项的方法:
    handleAdd () {
        const { input } = this.state
        const { addTodo } = this.props

        if (input) {
            addTodo(input)
            this.setState({ input: '' })
        }
    }
  1. 接下来,我们可以将状态值和处理程序方法分配给input字段和按钮:
    render () {
        const { input } = this.state

        return (
            <form onSubmit={e => { e.preventDefault(); this.handleAdd() }}>
                <input
                    type="text"
                    placeholder="enter new task..."
                    style={{ width: 350, height: 15 }}
 value={input}
 onChange={this.handleInput} />
                <input
                    type="submit"
                    style={{ float: 'right', marginTop: 2 }}
 disabled={!input}                    value="add"
                />
            </form>
        )
    }
  1. 最后,我们需要调整constructor以重新绑定所有处理程序方法的this上下文:
    constructor () {
        super(props)

        this.state = {
            input: ''
        }

 this.handleInput = this.handleInput.bind(this)
 this.handleAdd = this.handleAdd.bind(this)
    }

现在,我们的AddTodo组件将在没有输入文本时显示禁用的按钮。激活后,单击按钮将触发从App组件传递下来的handleAdd函数。

使 TodoList 组件动态

我们 ToDo 应用程序中的下一个组件是TodoList组件。在这里,我们只需要从StateContext中获取待办事项。

现在让我们让TodoList组件变得动态起来:

  1. src/TodoList.js中,我们首先导入StateContext,在TodoItem导入语句下面:
import StateContext from './StateContext'
  1. 然后,我们将contextType设置为StateContext,这将允许我们通过this.context访问上下文:
export default class TodoList extends React.Component {
 static contextType = StateContext

使用类组件,如果我们想要使用多个上下文,我们必须使用StateContext.Consumer组件,如下所示:<StateContext.Consumer>{value => <div>State is: {value}</div>}</StateContext.Consumer>

正如你所想象的那样,像这样使用多个上下文将导致非常深的组件树(包装器地狱),我们的代码将很难阅读和重构。

  1. 现在,我们可以从this.context中获取项目,而不是静态定义它们:
    render () {
 const items = this.context
  1. 最后,我们将所有 props 传递给TodoItem组件,以便我们可以在那里使用removeTodotoggleTodo方法:
        return items.map(item =>
            <TodoItem {...item} {...this.props} key={item.id} />
        )
    }

现在,我们的TodoList组件从StateContext中获取项目,而不是静态定义它们。

使 TodoItem 组件动态

现在我们已经将removeTodotoggleTodo方法作为 props 传递给了TodoItem组件,我们可以在那里实现这些功能。

现在让TodoItem组件变得动态起来:

  1. src/TodoItem.js中,我们首先定义了toggleTodoremoveTodo函数的处理程序方法:
    handleToggle () {
        const { toggleTodo, id } = this.props
        toggleTodo(id)
    }

    handleRemove () {
        const { removeTodo, id } = this.props
        removeTodo(id)
    }
  1. 然后,我们分别将处理程序方法分配给checkboxbutton
    render () {
        const { title, completed } = this.props
        return (
            <div style={{ width: 400, height: 25 }}>
                <input type="checkbox" checked={completed} onChange={this.handleToggle} />
                {title}
                <button style={{ float: 'right' }} onClick={this.handleRemove}>x</button>
            </div>
        )
    }
  1. 最后,我们需要重新绑定处理程序方法的this上下文。创建一个新的constructor,如下所示:
export default class TodoItem extends React.Component {
 constructor (props) {
 super(props)

 this.handleToggle = this.handleToggle.bind(this)
 this.handleRemove = this.handleRemove.bind(this)
 }

现在,TodoItem组件触发了切换和删除处理程序函数。

使 TodoFilter 组件动态

最后,我们将使用filterTodos方法动态过滤我们的待办事项列表。

让我们开始使TodoFilter组件动态:

  1. src/TodoFilter.js中,在TodoFilter类中,我们将所有 props 传递给TodoFilterItem组件:
export default class TodoFilter extends React.Component {
    render () {
        return (
            <div>
                <TodoFilterItem {...this.props} name="all" />{' / '}
                <TodoFilterItem {...this.props} name="active" />{' / '}
                <TodoFilterItem {...this.props} name="completed" />
            </div>
        )
    }
}
  1. src/TodoFilter.js中,在TodoFilterItem类中,我们首先定义一个用于设置过滤器的处理方法:
    handleFilter () {
        const { name, filterTodos } = this.props
        filterTodos(name)
    }
  1. 然后,我们从TodoFilter中获取filter prop:
    render () {
        const { name, filter = 'all' } = this.props
  1. 接下来,我们使用filter prop 来以bold显示当前选定的过滤器:
        const style = {
            color: 'blue',
            cursor: 'pointer',
            fontWeight: (filter === name) ? 'bold' : 'normal'
        }
  1. 然后,我们通过onClick将处理方法绑定到过滤器项:
        return <span style={style} onClick={this.handleFilter}>{name}</span>
    }
  1. 最后,我们为TodoFilterItem类创建一个新的constructor,并重新绑定处理方法的this上下文:
class TodoFilterItem extends React.Component {
 constructor (props) {
 super(props)

 this.handleFilter = this.handleFilter.bind(this)
 }

现在,我们的TodoFilter组件触发handleFilter方法以更改过滤器。现在我们的整个应用程序是动态的,我们可以使用其所有功能。

示例代码

示例代码可以在Chapter11/chapter11_1文件夹中找到。

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

从 React 类组件迁移

在使用 React 类组件设置示例项目之后,我们现在将把这个项目迁移到 React Hooks。我们将展示如何迁移副作用,比如在组件挂载时获取待办事项,以及我们用于输入的状态管理。

在本节中,我们将迁移以下组件:

  • TodoItem

  • TodoList

  • TodoFilterItem

  • TodoFilter

  • AddTodo

  • App

迁移 TodoItem 组件

最简单的组件之一要迁移的是TodoItem组件。它不使用任何状态或副作用,因此我们可以简单地将其转换为函数组件。

让我们开始迁移TodoItem组件:

  1. 编辑src/TodoItem.js并删除类组件代码。现在我们将定义一个函数组件。

  2. 我们首先定义一个函数,它接受五个 props——title值、completed布尔值、id值、toggleTodo函数和removeTodo函数:

export default function TodoItem ({ title, completed, id, toggleTodo, removeTodo }) {
  1. 接下来,我们定义我们的两个处理函数:
    function handleToggle () {
        toggleTodo(id)
    }

    function handleRemove () {
        removeTodo(id)
    }
  1. 最后,我们返回 JSX 代码以渲染我们的组件:
    return (
        <div style={{ width: 400, height: 25 }}>
            <input type="checkbox" checked={completed} onChange={handleToggle} />
            {title}
            <button style={{ float: 'right' }} onClick={handleRemove}>x</button>
        </div>
    )
}

尝试保持函数组件的规模较小,并通过创建包装它们的新函数组件来组合它们。拥有许多小组件而不是一个大组件总是一个好主意。它们更容易维护、重用和重构。

正如我们所看到的,函数组件不需要我们重新绑定this,或者根本不需要定义构造函数。此外,我们不需要多次从this.props中解构。我们可以简单地在函数的头部定义所有 props。

迁移 TodoList 组件

接下来,我们要迁移包裹TodoItem组件的TodoList组件。在这里,我们使用了一个上下文,这意味着我们现在可以使用上下文 Hook。

现在让我们迁移TodoList组件:

  1. 编辑src/TodoList.js并从 React 中导入useContext Hook:
import React, { useContext } from 'react'
  1. 移除类组件代码。我们现在要定义一个函数组件。

  2. 我们首先定义函数的头部。在这种情况下,我们不解构 props,而是简单地将它们存储在一个props对象中:

export default function TodoList (props) {
  1. 现在我们定义 Context Hook:
    const items = useContext(StateContext)
  1. 最后,我们返回渲染的items列表,使用解构将itemprops对象传递给它:
    return items.map(item =>
        <TodoItem {...item} {...props} key={item.id} />
    )
}

我们最后定义key属性,以避免在解构itemprops对象时覆盖它。

正如我们所看到的,使用 Hooks 与上下文更加直接。我们可以简单地调用一个函数,并使用返回值。当使用多个上下文时,不需要魔术赋值this.context或包装地狱!

此外,我们可以逐步将组件迁移到 React Hooks,并且我们的应用仍然可以工作。没有必要一次性将所有组件迁移到 Hooks。React 类组件可以很好地与使用 Hooks 的函数组件一起工作。唯一的限制是我们不能在类组件中使用 Hooks。因此,我们需要一次迁移一个完整的组件。

迁移 TodoFilter 组件

接下来是TodoFilter组件,它不会使用任何 Hooks。然而,我们将用两个函数组件替换TodoFilterItemTodoFilter组件:一个用于TodoFilterItem,一个用于TodoFilter组件。

迁移 TodoFilterItem

首先,我们要迁移TodoFilterItem组件。现在让我们开始迁移组件:

  1. 编辑src/TodoFilter.js并移除类组件代码。我们现在要定义一个函数组件。

  2. TodoFilterItem组件定义一个函数,它将接受三个 props——name值,filterTodos函数和filter值:

function TodoFilterItem ({ name, filterTodos, filter = 'all' }) {
  1. 在这个函数中,我们定义了一个处理器函数来改变过滤器:
    function handleFilter () {
        filterTodos(name)
    }
  1. 接下来,我们为我们的span元素定义一个style对象:
    const style = {
        color: 'blue',
        cursor: 'pointer',
        fontWeight: (filter === name) ? 'bold' : 'normal'
    }
  1. 最后,我们返回并渲染span元素:
    return <span style={style} onClick={handleFilter}>{name}</span>
}

正如我们所看到的,函数组件比相应的类组件需要更少的样板代码。

迁移 TodoFilter

现在我们已经迁移了TodoFilterItem组件,我们可以迁移TodoFilter组件。让我们现在迁移它:

  1. 编辑src/TodoFilter.js并删除类组件代码。我们现在要定义一个函数组件。

  2. TodoFilter组件定义一个函数。我们这里不会在 props 上使用解构:

export default function TodoFilter (props) {
  1. 在这个组件中,我们只返回和渲染三个TodoFilterItem组件 - 将 props 传递给它们:
    return (
        <div>
            <TodoFilterItem {...props} name="all" />{' / '}
            <TodoFilterItem {...props} name="active" />{' / '}
            <TodoFilterItem {...props} name="completed" />
        </div>
    )
}

现在,我们的TodoFilter组件已成功迁移。

迁移 AddTodo 组件

接下来,我们将迁移AddTodo组件。在这里,我们将使用 State Hook 来处理input字段状态。

让我们现在迁移AddTodo组件:

  1. 编辑src/AddTodo.js并调整导入语句以从 React 导入useState Hook:
import React, { useState } from 'react'
  1. 删除类组件代码。我们现在要定义一个函数组件。

  2. 首先,我们定义一个函数,它只接受一个 prop - addTodo函数:

export default function AddTodo ({ addTodo }) {
  1. 接下来,我们为input字段状态定义一个 State Hook:
    const [ input, setInput ] = useState('')
  1. 现在,我们可以为input字段和添加按钮定义处理函数:
    function handleInput (e) {
        setInput(e.target.value)
    }

    function handleAdd () {
        if (input) {
            addTodo(input)
            setInput('')
        }
    }
  1. 最后,我们返回并渲染input字段和添加按钮:
    return (
        <form onSubmit={e => { e.preventDefault(); handleAdd() }}>
            <input
                type="text"
                placeholder="enter new task..."
                style={{ width: 350, height: 15 }}
                value={input}
                onChange={handleInput}
            />
            <input
                type="submit"
                style={{ float: 'right', marginTop: 2 }}
                disabled={!input}
                value="add"
            />
        </form>
    )
}

正如我们所看到的,使用 State Hook 使状态管理变得更加简单。我们可以为每个状态值定义一个单独的值和 setter 函数,而不是不断处理一个状态对象。此外,我们不需要一直从this.state中解构。因此,我们的代码更加清晰简洁。

迁移 App 组件

最后,剩下的就是迁移App组件。然后,我们整个待办事项应用程序将被迁移到 React Hooks。在这里,我们将使用 Reducer Hook 来管理状态,Effect Hook 在组件挂载时获取待办事项,以及 Memo Hook 来存储过滤后的待办事项列表。

在本节中,我们将做以下事情:

  • 定义动作

  • 定义 reducers

  • 迁移App组件

定义动作

我们的应用将接受五个动作:

  • FETCH_TODOS:获取新的待办事项列表 - { type: 'FETCH_TODOS', todos: [] }

  • ADD_TODO:插入新的待办事项 - { type: 'ADD_TODO', title: 'Test ToDo app' }

  • TOGGLE_TODO:切换待办事项的completed值 - { type: 'TOGGLE_TODO', id: 'xxx' }

  • REMOVE_TODO:移除一个待办事项—{ type: 'REMOVE_TODO', id: 'xxx' }

  • FILTER_TODOS:过滤待办事项—{ type: 'FILTER_TODOS', filter: 'completed' }

在定义完动作之后,我们可以继续定义 reducers。

定义 reducers

现在我们要为我们的状态定义 reducers。我们需要一个 app reducer 和两个子 reducer:一个用于 todos,一个用于 filter。

过滤后的待办事项列表将由App组件动态计算。我们可以稍后使用 Memo Hook 来缓存结果,避免不必要地重新计算过滤后的待办事项列表。

定义 filter reducer

我们将首先定义filter值的 reducer。现在让我们定义 filter reducer:

  1. 创建一个新的src/reducers.js文件,并从src/api.js文件中导入generateID函数:
import { generateID } from './api'
  1. src/reducers.js文件中,定义一个新函数,它将处理FILTER_TODOS动作,并相应地设置值:
function filterReducer (state, action) {
    if (action.type === 'FILTER_TODOS') {
        return action.filter
    } else {
        return state
    }
}

现在,filterReducer函数已经定义,我们可以正确处理FILTER_TODOS动作了。

定义 todos reducer

接下来,我们将为待办事项定义一个函数。在这里,我们将处理FETCH_TODOSADD_TODOTOGGLE_TODOREMOVE_TODO动作。

现在让我们定义todosReducer函数:

  1. src/reducers.js文件中,定义一个新函数,它将处理这些动作:
function todosReducer (state, action) {
    switch (action.type) {
  1. 对于FETCH_TODOS动作,我们只需用新的todos数组替换当前状态:
        case 'FETCH_TODOS':
            return action.todos
  1. 对于ADD_TODO动作,我们将在当前状态数组的开头插入一个新项目:
        case 'ADD_TODO':
            const newTodo = {
                id: generateID(),
                title: action.title,
                completed: false
            }
            return [ newTodo, ...state ]
  1. 对于TOGGLE_TODO动作,我们将使用map函数来更新单个待办事项:
        case 'TOGGLE_TODO':
            return state.map(t => {
                if (t.id === action.id) {
                    return { ...t, completed: !t.completed }
                }
                return t
            }, [])
  1. 对于REMOVE_TODO动作,我们将使用filter函数来移除单个待办事项:
        case 'REMOVE_TODO':
            return state.filter(t => {
                if (t.id === action.id) {
                    return false
                }
                return true
            })
  1. 默认情况下(对于所有其他动作),我们只需返回当前的state
        default:
            return state
    }
}

现在,todos reducer 已经定义,我们可以处理FETCH_TODOSADD_TODOTOGGLE_TODOREMOVE_TODO动作了。

定义 app reducer

最后,我们需要将其他 reducers 组合成一个单一的 app 状态的 reducer。现在让我们定义appReducer函数:

  1. src/reducers.js文件中,为appReducer定义一个新函数:
export default function appReducer (state, action) {
  1. 在这个函数中,我们返回一个包含其他 reducers 值的对象。我们只需将子状态和动作传递给其他 reducers:
    return {
        todos: todosReducer(state.todos, action),
        filter: filterReducer(state.filter, action)
    }
}

现在,我们的 reducers 已经分组在一起。所以,我们只有一个state对象和一个dispatch函数。

组件迁移

现在我们已经定义了我们的 reducers,我们可以开始迁移App组件。让我们现在迁移它:

  1. 编辑src/App.js并调整导入语句,从React中导入useReduceruseEffectuseMemo
import React, { useReducer, useEffect, useMemo } from 'react'
  1. src/reducers.js中导入appReducer函数:
import appReducer from './reducers'
  1. 删除类组件代码。现在我们要定义一个函数组件。

  2. 首先,我们定义一个不接受任何 props 的函数:

export default function App () {
  1. 现在,我们使用appReducer函数定义一个 Reducer Hook:
    const [ state, dispatch ] = useReducer(appReducer, { todos: [], filter: 'all' })
  1. 接下来,我们定义一个 Effect Hook,它将通过 API 函数获取todos,然后将会派发一个FETCH_TODOS动作:
    useEffect(() => {
        fetchAPITodos().then((todos) =>
            dispatch({ type: 'FETCH_TODOS', todos })
        )
    }, [])
  1. 然后,我们使用 Memo Hook 实现过滤机制,以优化性能并避免在没有变化时重新计算过滤后的 todos 列表:
    const filteredTodos = useMemo(() => {
        const { filter, todos } = state
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }, [ state ])
  1. 现在,我们定义各种函数,这些函数将派发动作并改变状态:
    function addTodo (title) {
        dispatch({ type: 'ADD_TODO', title })
    }

    function toggleTodo (id) {
        dispatch({ type: 'TOGGLE_TODO', id })
    }

    function removeTodo (id) {
        dispatch({ type: 'REMOVE_TODO', id })
    }

    function filterTodos (filter) {
        dispatch({ type: 'FILTER_TODOS', filter })
    }
  1. 最后,我们返回并渲染所有需要的 ToDo 应用程序组件:
    return (
        <StateContext.Provider value={filteredTodos}>
            <div style={{ width: 400 }}>
                <Header />
                <AddTodo addTodo={addTodo} />
                <hr />
                <TodoList toggleTodo={toggleTodo} removeTodo={removeTodo} />
                <hr />
                <TodoFilter filter={state.filter} filterTodos={filterTodos} />
            </div>
        </StateContext.Provider>
    )
}

正如我们所看到的,使用 reducer 来处理复杂的状态变化使我们的代码更加简洁和易于维护。我们的应用现在完全迁移到了 Hooks!

示例代码

示例代码可以在Chapter11/chapter11_2文件夹中找到。

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

类组件的权衡

现在我们已经完成了从类组件到 Hooks 的迁移,让我们回顾和总结我们学到的东西。

通过计算代码行数,我们可以看到,总共有 392 行 JavaScript 代码的函数组件与 Hooks 比起需要 430 行 JavaScript 代码的类组件更加简洁。此外,函数组件与 Hooks 更容易理解和测试,因为它们只是使用 JavaScript 函数而不是复杂的 React 构造。此外,我们能够将所有的状态改变逻辑重构到一个单独的reducers.js文件中,从而将其与App组件解耦,并使其更容易重构和测试。这将App.js文件的大小从 109 行减少到 64 行,并在reducers.js文件中增加了 50 行。

我们可以在下表中看到减少的代码行数:

比较:JavaScript 代码行数
类组件

| 36  ./TodoFilter.js 15  ./TodoList.js

59  ./AddTodo.js

12  ./index.js

7   ./Header.js

5   ./StateContext.js

9   ./App.test.js

135 ./serviceWorker.js

12  ./api.js

109 ./App.js

31  ./TodoItem.js | 25  ./TodoFilter.js 12  ./TodoList.js

42  ./AddTodo.js

12  ./index.js

7   ./Header.js

50  ./reducers.js

5   ./StateContext.js

9   ./App.test.js

135 ./serviceWorker.js

12  ./api.js

64  ./App.js

19  ./TodoItem.js |

430 总计 392 总计

使用函数组件和 Hooks,以下几点不需要考虑:

  • 不需要处理构造函数

  • 没有混淆的 this 上下文(this 重新绑定)

  • 不需要一遍又一遍地解构相同的值

  • 在处理上下文、props 和状态时没有魔法

  • 如果我们想在 props 改变时重新获取数据,就不需要定义 componentDidMountcomponentDidUpdate

此外,函数组件具有以下优势:

  • 鼓励创建小而简单的组件

  • 更容易重构

  • 更容易测试

  • 需要更少的代码

  • 对初学者更容易理解

  • 更具声明性

然而,在以下情况下,类组件可能是可以的:

  • 遵循某些约定时。

  • 使用最新的 JavaScript 特性来避免 this 重新绑定。

  • 可能更容易理解,因为团队已经掌握了相关知识。

  • 许多项目仍在使用类。对于库来说,这不是什么问题,因为它们可以很好地与函数组件一起工作。在工作中,你可能需要使用类。

  • 根据 React 团队的说法,类不会很快从 React 中移除。

最后,这是一个偏好的问题,但是 Hooks 确实比类有很多优势!如果你正在开始一个新项目,一定要选择 Hooks。如果你正在处理一个现有项目,考虑是否有必要重构某些组件为基于 Hook 的组件,以使它们更简单。然而,你不应该立即将所有项目都转换为 Hooks,因为重构总是可能引入新的 bug。采用 Hooks 的最佳方式是在适当的时候,慢慢但确定地用基于 Hook 的函数组件替换旧的类组件。例如,如果你已经在重构一个组件,你可以将其重构为使用 Hooks!

总结

在本章中,我们首先使用 React 类组件构建了一个 ToDo 应用程序。我们首先设计了应用程序结构,然后实现了静态组件,最后使它们变得动态。在接下来的部分,我们学习了如何将使用类组件的现有项目迁移到使用 Hooks 的函数组件。最后,我们学习了类组件的权衡,何时应该使用类组件或 Hooks,以及如何迁移现有项目到 Hooks。

我们现在已经实际看到了 React 类组件与使用 Hooks 的函数组件的不同之处。Hooks 使我们的代码更加简洁,更易于阅读和维护。我们还学到了应该逐步将我们的组件从类组件迁移到使用 Hooks 的函数组件——没有必要立即迁移整个应用程序。

在下一章中,我们将学习如何使用 Redux 处理状态,使用 Redux 与仅使用 Hooks 的函数组件相比的权衡,如何在 Hooks 中使用 Redux,以及如何将现有的 Redux 应用程序迁移到基于 Hook 的设置。

问题

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

  1. React 类组件是如何定义的?

  2. 在使用类组件的constructor时,我们需要调用什么?为什么?

  3. 我们如何在类组件中设置初始状态?

  4. 我们如何在类组件中改变状态?

  5. 为什么我们需要重新绑定类组件方法的this上下文?

  6. 我们如何重新绑定this上下文?

  7. 我们如何在类组件中使用 React 上下文?

  8. 在迁移到 Hooks 时,我们可以用什么替代状态管理?

  9. 使用 Hooks 与类组件相比有什么权衡之处?

  10. 何时以及如何迁移现有项目到 Hooks?

进一步阅读

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

第十二章:Redux 和 Hooks

在上一章中,我们学习了关于 React 类组件,以及如何从现有的基于类组件的项目迁移到基于 Hook 的项目。然后,我们了解了两种解决方案之间的权衡,并讨论了现有项目应该何时以及如何迁移。

在本章中,我们将把上一章中创建的 ToDo 应用程序转换为 Redux 应用程序。首先,我们将学习 Redux 是什么,包括 Redux 的三个原则。我们还将学习在应用程序中何时使用 Redux 是有意义的,以及它并不适用于每个应用程序。此外,我们将学习如何使用 Redux 处理状态。之后,我们将学习如何在 Hooks 中使用 Redux,以及如何将现有的 Redux 应用程序迁移到 Hooks。最后,我们将学习 Redux 的权衡,以便能够决定哪种解决方案对于特定用例最合适。通过本章结束时,您将完全了解如何使用 Hooks 编写 Redux 应用程序。

本章将涵盖以下主题:

  • Redux 是什么,以及何时以及为什么应该使用它

  • 使用 Redux 处理状态

  • 使用 Hooks 与 Redux

  • 迁移 Redux 应用程序

  • 学习 Redux 的权衡

技术要求

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

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

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

bit.ly/2Mm9yoC

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

现在,让我们开始本章。

Redux 是什么?

正如我们之前学到的,应用程序中有两种状态:

  • 本地 状态:例如,处理输入字段数据

  • 全局 状态:例如,存储当前登录的用户

在本书中,我们使用 State Hook 处理本地状态,使用 Reducer Hook 处理更复杂的状态(通常是全局状态)。

Redux 是一种可以用来处理 React 应用程序中各种状态的解决方案。它提供了一个包含所有应用程序状态的单个状态树对象。这与我们在博客应用程序中使用的 Reducer Hook 类似。传统上,Redux 也经常用于存储本地状态,这使得状态树非常复杂。

Redux 本质上由五个元素组成:

  • 存储:包含状态的对象,描述了我们应用程序的完整状态—{ todos: [], filter: 'all' }

  • 动作:描述状态修改的对象—{ type: 'FILTER_TODOS', filter: 'completed' }

  • 动作创建者:创建动作对象的函数—(filter) => ({ type: 'FILTER_TODOS', filter })

  • 减速器:接受当前 state 值和一个 action 对象,并返回一个新状态的函数—(state, action) => { ... }

  • 连接器:将现有组件连接到 Redux 的高阶组件,通过将 Redux 状态和动作创建者注入为 props—connect(mapStateToProps, mapDispatchToProps)(Component)

在 Redux 生命周期中,存储 包含定义 UI 的状态。UI 通过 连接器 连接到 Redux 存储。用户与 UI 的交互触发 动作,然后发送到 减速器减速器 然后更新 存储 中的状态。

我们可以在下图中看到 Redux 生命周期的可视化:

Redux 生命周期的可视化

正如你所看到的,我们已经了解了这些组件中的三个:store(状态树)、动作和减速器。Redux 就像是 Reducer Hook 的更高级版本。不同之处在于,使用 Redux,我们总是将状态分派给单个减速器,因此只改变一个状态。Redux 不应该有多个实例。通过这种限制,我们可以确保整个应用程序状态都包含在一个对象中,这使我们能够仅从 Redux 存储中重建整个应用程序状态。

由于只有一个包含所有状态的存储,我们可以通过在崩溃报告中保存 Redux 存储,或者在调试过程中自动重放某些操作来轻松调试错误的状态,这样我们就不需要手动输入文本和点击按钮,一遍又一遍。此外,Redux 提供了简化我们处理异步请求的中间件,例如从服务器获取数据。现在我们了解了 Redux 是什么,在下一节中,我们将学习 Redux 的三个基本原则。

Redux 的三个原则

Redux 的 API 非常小,实际上只包含少数几个函数。Redux 如此强大的原因在于在使用该库时应用于代码的一套规则。这些规则允许编写可扩展、易于扩展、测试和调试的应用程序。

Redux 基于三个基本原则:

  • 真相的单一来源

  • 只读状态

  • 状态更改通过纯函数处理

真相的单一来源

这个 Redux 原则指出数据应该始终有一个单一的真相来源。这意味着全局数据来自单一的 Redux 存储,本地数据来自,例如,某个 State Hook。每种数据只有一个来源。因此,应用程序变得更容易调试,更不容易出错。

只读状态

使用 Redux,不可能直接修改应用程序状态。只能通过分派动作来改变状态。这个原则使状态变化可预测:如果没有动作发生,应用程序状态将不会改变。此外,动作是逐个处理的,因此我们不必处理竞争条件。最后,动作是纯粹的 JavaScript 对象,这使它们易于序列化、记录、存储或重放。因此,调试和测试 Redux 应用程序变得非常容易。

状态更改通过纯函数处理

纯函数是指,给定相同的输入,将始终返回相同的输出。Redux 中的 Reducer 函数是纯的,因此,给定相同的状态和动作,它们将始终返回相同的新状态。

例如,以下的 reducer 是一个不纯的函数,因为多次使用相同的输入调用该函数会产生不同的输出:

let i = 0
function counterReducer (state, action) {
    if (action.type === 'INCREMENT') {
        i++
    }
    return i
}

console.log(counterReducer(0, { type: 'INCREMENT' })) // prints 1
console.log(counterReducer(0, { type: 'INCREMENT' })) // prints 2

要将这个 reducer 转变为纯函数,我们必须确保它不依赖于外部状态,只使用其参数进行计算:

function counterReducer (state, action) {
    if (action.type === 'INCREMENT') {
        return state + 1
    }
    return state
}

console.log(counterReducer(0, { type: 'INCREMENT' })) // prints 1
console.log(counterReducer(0, { type: 'INCREMENT' })) // prints 1

使用纯函数进行减速器使它们可预测,易于测试和调试。使用 Redux 时,我们需要小心始终返回新状态,而不是修改现有状态。因此,例如,我们不能在数组状态上使用Array.push(),因为它会修改现有数组;我们必须使用Array.concat()来创建一个新数组。对于对象也是一样,我们必须使用 rest/spread 语法来创建新对象,而不是修改现有对象。例如,{ ...state, completed: true }

现在我们已经了解了 Redux 的三个基本原则,我们可以开始在我们的 ToDo 应用程序中实践使用 Redux 来实现状态处理。

使用 Redux 处理状态

使用 Redux 进行状态管理实际上与使用 Reducer Hook 非常相似。我们首先定义状态对象,然后操作,最后是我们的减速器。Redux 中的另一个模式是创建返回操作对象的函数,即所谓的操作创建者。此外,我们需要用Provider组件包装整个应用程序,并连接组件到 Redux 存储,以便能够使用 Redux 状态和操作创建者。

安装 Redux

首先,我们必须安装 Redux,React Redux 和 Redux Thunk。让我们分别看看每个库的作用:

  • Redux 本身只处理 JavaScript 对象,因此它提供存储,处理操作和操作创建者,并处理减速器。

  • React Redux 提供连接器,以便将 Redux 连接到我们的 React 组件。

  • Redux Thunk 是一个中间件,允许我们在 Redux 中处理异步请求。

React中与Redux结合使用Redux可以将全局状态管理转移到Redux,而React处理应用程序的渲染和本地状态:

React 和 Redux 如何一起工作的示例

要安装 Redux 和 React Redux,我们将使用npm。执行以下命令:

> npm install --save redux react-redux redux-thunk

现在所有必需的库都已安装好,我们可以开始设置我们的 Redux 存储。

定义状态、操作和减速器

开发 Redux 应用程序的第一步是定义状态,然后是将改变状态的操作,最后是执行状态修改的减速器函数。在我们的 ToDo 应用程序中,我们已经定义了状态,操作和减速器,以便使用 Reducer Hook。在这里,我们只是简单地回顾了我们在上一章中定义的内容。

状态

我们 ToDo 应用程序的完整状态对象由两个键组成:一个 todo 项目数组和一个字符串,用于指定当前选择的filter值。初始状态如下:

{
    "todos": [
        { "id": 1, "title": "Write React Hooks book", "completed": true },
        { "id": 2, "title": "Promote book", "completed": false }
    ],
    "filter": "all"
}

正如我们所看到的,在 Redux 中,状态对象包含了对我们应用程序重要的所有状态。在这种情况下,应用程序状态由一个todos数组和一个filter字符串组成。

动作

我们的应用程序接受以下五个动作:

  • FETCH_TODOS:获取新的 todo 项目列表——{ type: 'FETCH_TODOS', todos: [] }

  • ADD_TODO:插入新的 todo 项目——{ type: 'ADD_TODO', title: 'Test ToDo app' }

  • TOGGLE_TODO:切换 todo 项目的completed值——{ type: 'TOGGLE_TODO', id: 'xxx' }

  • REMOVE_TODO:移除 todo 项目——{ type: 'REMOVE_TODO', id: 'xxx' }

  • FILTER_TODOS:过滤 todo 项目——{ type: 'FILTER_TODOS', filter: 'completed' }

Reducers

我们定义了三个 reducer——分别用于我们状态的每个部分,并且定义了一个 app reducer 来合并其他两个 reducer。filter reducer 等待FILTER_TODOS动作,然后相应地设置新的过滤器。todos reducer 监听其他与 todo 相关的动作,并通过添加、删除或修改元素来调整 todos 数组。然后 app reducer 合并这两个 reducer,并将动作传递给它们。在定义了创建 Redux 应用所需的所有元素之后,我们现在可以设置 Redux 存储。

设置 Redux 存储

为了最初保持简单,并展示 Redux 的工作原理,我们现在不会使用连接器。我们只是简单地用 Redux 替换了之前由 Reducer Hook 提供的state对象和dispatch函数。

现在让我们设置 Redux 存储:

  1. 编辑src/App.js,并从 Redux 库中导入useState Hook 和createStore函数:
import React, { useState, useEffect, useMemo } from 'react'
import { createStore } from 'redux' 
  1. 在导入语句之后,并在App函数定义之前,我们将初始化 Redux 存储。我们首先定义初始状态:
const initialState = { todos: [], filter: 'all' }
  1. 接下来,我们将使用createStore函数来定义 Redux 存储,通过使用现有的appReducer函数并传递initialState对象:
const store = createStore(appReducer, initialState)

请注意,在 Redux 中,通过将其传递给createStore来初始化状态并不是最佳实践。然而,在 Reducer Hook 中,我们需要以这种方式进行。在 Redux 中,我们通常通过在 reducer 函数中设置默认值来初始化状态。我们将在本章后面学习更多关于通过 Redux reducer 初始化状态的内容。

  1. 现在,我们可以从存储中获取dispatch函数:
const { dispatch } = store
  1. 下一步是在App函数中删除以下 Reducer Hook 定义:
    const [ state, dispatch ] = useReducer(appReducer, { todos: [], filter: 'all' })

它被一个简单的 State Hook 替换,它将存储我们的 Redux 状态:

    const [ state, setState ] = useState(initialState)
  1. 最后,我们定义一个 Effect Hook,以便将 State Hook 与 Redux 存储状态同步:
    useEffect(() => {
        const unsubscribe = store.subscribe(() => setState(store.getState()))
        return unsubscribe
    }, [])

正如我们所看到的,应用程序仍然以与以前完全相同的方式运行。Redux 的工作方式与 Reducer Hook 非常相似,但具有更多的功能。然而,在如何定义动作和减速器方面有轻微的差异,我们将在接下来的章节中学习。

示例代码

示例代码可以在Chapter12/chapter12_1文件夹中找到。

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

定义动作类型

创建完整的 Redux 应用程序的第一步是定义所谓的动作类型。它们将用于在动作创建者中创建动作,并在减速器中处理动作。这里的想法是避免在定义或比较动作的type属性时出现拼写错误。

现在让我们定义动作类型:

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

  2. 在新创建的文件中定义并导出以下常量:

export const FETCH_TODOS = 'FETCH_TODOS'
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const REMOVE_TODO = 'REMOVE_TODO'
export const FILTER_TODOS = 'FILTER_TODOS'

现在我们已经定义了我们的动作类型,我们可以开始在动作创建者和减速器中使用它们。

定义动作创建者

在定义动作类型之后,我们需要定义动作本身。这样做,我们将定义返回动作对象的函数。这些函数被称为动作创建者,有两种类型:

  • 同步 动作创建者:这些只是返回一个动作对象

  • 异步 动作创建者:这些返回一个async函数,稍后将调度一个动作

我们将首先定义同步动作创建者,然后我们将学习如何定义异步动作创建者。

定义同步动作创建者

我们已经在src/App.js中早些时候定义了动作创建者函数。现在我们可以从我们的App组件中复制它们,确保我们调整type属性以使用动作类型常量,而不是静态字符串。

现在让我们定义同步动作创建者:

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

  2. 导入我们将需要创建动作的所有动作类型:

import {
    ADD_TODO, TOGGLE_TODO, REMOVE_TODO, FILTER_TODOS
} from './actionTypes'
  1. 现在,我们可以定义并导出我们的动作创建者函数:
export function addTodo (title) {
    return { type: ADD_TODO, title }
}

export function toggleTodo (id) {
    return { type: TOGGLE_TODO, id }
}

export function removeTodo (id) {
    return { type: REMOVE_TODO, id }
}

export function filterTodos (filter) {
    return { type: FILTER_TODOS, filter }
}

正如我们所看到的,同步动作创建者只是创建并返回动作对象。

定义异步动作创建者

下一步是为fetchTodos动作定义一个异步动作创建者。在这里,我们将使用async/await结构。

现在我们将使用async函数来定义fetchTodos动作创建者:

  1. src/actions.js中,首先导入FETCH_TODOS动作类型和fetchAPITodos函数:
import {
    FETCH_TODOS, ADD_TODO, TOGGLE_TODO, REMOVE_TODO, FILTER_TODOS
} from './actionTypes'
import { fetchAPITodos } from './api'
  1. 然后,定义一个新的动作创建者函数,该函数将返回一个获取dispatch函数作为参数的async函数:
export function fetchTodos () {
    return async (dispatch) => {
  1. 在这个async函数中,我们现在将调用 API 函数,并dispatch我们的动作:
        const todos = await fetchAPITodos()
        dispatch({ type: FETCH_TODOS, todos })
    }
}

正如我们所看到的,异步动作创建者返回一个函数,该函数将在以后的时间调度动作。

调整存储

为了我们能够在 Redux 中使用异步动作创建者函数,我们需要加载redux-thunk中间件。此中间件检查动作创建者是否返回一个函数,而不是一个普通对象,如果是这种情况,它会执行该函数,并将dispatch函数作为参数传递给它。

现在让我们调整存储以允许异步动作创建者:

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

  2. 从 Redux 中导入createStoreapplyMiddleware函数:

import { createStore, applyMiddleware } from 'redux'
  1. 接下来,导入thunk中间件和appReducer函数:
import thunk from 'redux-thunk'

import appReducer from './reducers'
  1. 现在,我们可以定义存储并将thunk中间件应用于它:
const store = createStore(appReducer, applyMiddleware(thunk))
  1. 最后,我们导出store
export default store

使用redux-thunk中间件,我们现在可以调度稍后调度动作的函数,这意味着我们的异步动作创建者现在将正常工作。

调整 reducers

如前所述,Redux reducers 与 Reducer Hooks 不同,它们具有某些约定:

  • 每个 reducer 需要通过在函数定义中定义默认值来设置其初始状态

  • 每个 reducer 需要返回未处理动作的当前状态

现在我们将调整现有的 reducers,使它们遵循这些约定。第二个约定已经实现了,因为我们之前定义了一个单一的应用程序 reducer,以避免有多个 dispatch 函数。

在 Redux reducers 中设置初始状态

因此,我们将专注于第一个约定-通过在函数参数中定义默认值来设置初始状态,如下所示:

  1. 编辑src/reducers.js并从 Redux 中导入combineReducers函数:
import { combineReducers } from 'redux'
  1. 然后,将filterReducer重命名为filter,并设置默认值:
function filter (state = 'all', action) {
  1. 接下来,在todosReducer中进行相同的编辑和重复相同的过程:
function todos (state = [], action) {
  1. 最后,我们将使用combineReducers函数来创建我们的appReducer函数。现在我们可以这样做,而不是手动创建函数:
const appReducer = combineReducers({ todos, filter })
export default appReducer

正如我们所看到的,Redux reducers 非常类似于 Reducer Hooks。Redux 甚至提供了一个函数,允许我们将多个 reducer 函数组合成一个单一的应用 reducer!

连接组件

现在是时候介绍连接器和容器组件了。在 Redux 中,我们可以使用connect高阶组件将现有的组件连接到 Redux,通过将状态和动作创建者作为 props 注入到它们中。

Redux 定义了两种不同类型的组件:

  • 表示性 组件:就像我们一直定义的 React 组件

  • 容器 组件:连接表示性组件到 Redux 的 React 组件

容器组件使用连接器将 Redux 连接到表示性组件。这个连接器接受两个函数:

  • mapStateToProps(state): 获取当前的 Redux 状态,并返回一个要传递给组件的 props 对象;用于将状态传递给组件

  • mapDispatchToProps(dispatch): 从 Redux 存储中获取dispatch函数,并返回一个要传递给组件的 props 对象;用于将动作创建者传递给组件

我们现在将为现有的表示性组件定义容器组件:

  1. 首先,为所有我们的表示性组件创建一个新的src/components/文件夹。

  2. 然后,我们将所有现有的组件文件复制到src/components/文件夹,并调整以下文件的导入语句:AddTodo.jsApp.jsHeader.jsTodoFilter.jsTodoItem.jsTodoList.js

连接 AddTodo 组件

我们现在将开始连接我们的组件到 Redux 存储。表示性组件可以保持与以前相同。我们只创建新的组件—容器组件—将表示性组件包装起来,并向它们传递特定的 props。

现在让我们连接AddTodo组件:

  1. 为所有我们的容器组件创建一个新的src/containers/文件夹。

  2. 创建一个新的src/containers/ConnectedAddTodo.js文件。

  3. 在这个文件中,我们从react-redux中导入connect函数,从redux中导入bindActionCreators函数:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入addTodo动作创建者和AddTodo组件:
import { addTodo } from '../actions'
import AddTodo from '../components/AddTodo'
  1. 现在,我们将定义mapStateToProps函数。由于这个组件不涉及 Redux 中的任何状态,我们可以在这里简单地返回一个空对象:
function mapStateToProps (state) {
    return {}
}
  1. 然后,我们定义mapDispatchToProps函数。在这里,我们使用bindActionCreators将动作创建者与dispatch函数包装起来:
function mapDispatchToProps (dispatch) {
    return bindActionCreators({ addTodo }, dispatch)
}

这段代码本质上与手动包装动作创建者是一样的,如下所示:

function mapDispatchToProps (dispatch) {
    return {
        addTodo: (...args) => dispatch(addTodo(...args))
    }
}
  1. 最后,我们使用connect函数将AddTodo组件连接到 Redux:
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo)

现在,我们的AddTodo组件成功连接到了 Redux 存储。

连接 TodoItem 组件

接下来,我们将连接TodoItem组件,以便在下一步中在TodoList组件中使用它。

现在让我们连接TodoItem组件:

  1. 创建一个新的src/containers/ConnectedTodoItem.js文件。

  2. 在这个文件中,我们从react-redux中导入connect函数,从redux中导入bindActionCreators函数:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入toggleTodoremoveTodo动作创建者,以及TodoItem组件:
import { toggleTodo, removeTodo } from '../actions'
import TodoItem from '../components/TodoItem'
  1. 同样,我们只从mapStateToProps中返回一个空对象:
function mapStateToProps (state) {
    return {}
}
  1. 这一次,我们将两个动作创建者绑定到dispatch函数:
function mapDispatchToProps (dispatch) {
    return bindActionCreators({ toggleTodo, removeTodo }, dispatch)
}
  1. 最后,我们连接组件,并导出它:
export default connect(mapStateToProps, mapDispatchToProps)(TodoItem)

现在,我们的TodoItem组件成功连接到了 Redux 存储。

连接 TodoList 组件

连接TodoItem组件之后,我们现在可以在TodoList组件中使用ConnectedTodoItem组件。

现在让我们连接TodoList组件:

  1. 编辑src/components/TodoList.js,并调整导入语句如下:
import ConnectedTodoItem from '../containers/ConnectedTodoItem'
  1. 然后,将从函数返回的组件重命名为ConnectedTodoItem
    return filteredTodos.map(item =>
        <ConnectedTodoItem {...item} key={item.id} />
    )
  1. 现在,创建一个新的src/containers/ConnectedTodoList.js文件。

  2. 在这个文件中,我们只从react-redux中导入connect函数,因为这一次我们不打算绑定动作创建者:

import { connect } from 'react-redux'
  1. 接下来,我们导入TodoList组件:
import TodoList from '../components/TodoList'
  1. 现在,我们定义mapStateToProps函数。这一次,我们使用解构从state对象中获取todosfilter,然后返回它们:
function mapStateToProps (state) {
    const { filter, todos } = state
    return { filter, todos }
}
  1. 接下来,我们定义mapDispatchToProps函数,在这里我们只返回一个空对象,因为我们不打算将任何动作创建者传递给TodoList组件:
function mapDispatchToProps (dispatch) {
    return {}
}
  1. 最后,我们连接并导出连接的TodoList组件:
export default connect(mapStateToProps, mapDispatchToProps)(TodoList)

现在,我们的TodoList组件成功连接到了 Redux 存储。

调整 TodoList 组件

现在我们已经连接了TodoList组件,我们可以将App组件中的过滤逻辑移动到TodoList组件中,如下所示:

  1. src/components/TodoList.js中导入useMemo Hook:
import  React,  {  useMemo  }  from  'react'
  1. 编辑src/components/App.js,并删除以下代码:
    const filteredTodos = useMemo(() => {
        const { filter, todos } = state
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }, [ state ])
  1. 现在,编辑src/components/TodoList.js,并在这里添加filteredTodos代码。请注意,我们从状态对象中删除了解构,因为组件已经以 props 接收了filtertodos值。我们还相应地调整了依赖数组:
    const filteredTodos = useMemo(() => {
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }, [ filter, todos ])

现在,我们的过滤逻辑在TodoList组件中,而不是App组件中。让我们继续连接其余的组件。

连接TodoFilter组件

接下来是TodoFilter组件。在这里,我们将使用mapStateToPropsmapDispatchToProps

现在让我们连接TodoFilter组件:

  1. 创建一个新的src/containers/ConnectedTodoFilter.js文件。

  2. 在这个文件中,我们从react-redux中导入connect函数,从redux中导入bindActionCreators函数:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入filterTodos动作创建者和TodoFilter组件:
import { filterTodos } from '../actions'
import TodoFilter from '../components/TodoFilter'
  1. 我们使用解构从我们的state对象中获取filter,然后返回它:
function mapStateToProps (state) {
    const { filter } = state
    return { filter }
}
  1. 接下来,我们绑定并返回filterTodos动作创建者:
function mapDispatchToProps (dispatch) {
    return bindActionCreators({ filterTodos }, dispatch)
}
  1. 最后,我们连接组件并导出它:
export default connect(mapStateToProps, mapDispatchToProps)(TodoFilter)

现在,我们的TodoFilter组件已成功连接到 Redux 存储。

连接 App 组件

现在唯一还需要连接的组件是App组件。在这里,我们将注入fetchTodos动作创建者,并更新组件以使用所有其他组件的连接版本。

现在让我们连接App组件:

  1. 编辑src/components/App.js,并调整以下导入语句:
import ConnectedAddTodo from '../containers/ConnectedAddTodo'
import ConnectedTodoList from '../containers/ConnectedTodoList'
import ConnectedTodoFilter from '../containers/ConnectedTodoFilter'
  1. 还要调整从函数返回的以下组件:
            return (
                <div style={{ width: 400 }}>
                    <Header />
                    <ConnectedAddTodo />
                    <hr />
                    <ConnectedTodoList />
                    <hr />
                    <ConnectedTodoFilter />
                </div>
            )
  1. 现在,我们可以创建连接的组件。创建一个新的src/containers/ConnectedApp.js文件。

  2. 在这个新创建的文件中,我们从react-redux中导入connect函数,从redux中导入bindActionCreators函数:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入fetchTodos动作创建者和App组件:
import { fetchTodos } from '../actions'
import App from '../components/App'
  1. 我们已经在其他组件中处理了状态的各个部分,因此无需将任何状态注入我们的App组件中:
function mapStateToProps (state) {
    return {}
}
  1. 然后,我们绑定并返回fetchTodos动作创建者:
function mapDispatchToProps (dispatch) {
    return bindActionCreators({ fetchTodos }, dispatch)
}
  1. 最后,我们连接App组件并导出它:
export default connect(mapStateToProps, mapDispatchToProps)(App)

现在,我们的App组件已成功连接到 Redux 存储。

设置 Provider 组件

最后,我们必须设置一个Provider组件,它将为 Redux 存储提供上下文,这将被连接器使用。

现在让我们设置Provider组件:

  1. 编辑src/index.js,并从react-redux中导入Provider组件:
import { Provider } from 'react-redux'
  1. 现在,从containers文件夹中导入ConnectedApp组件,并导入由configureStore.js创建的 Redux 存储:
import ConnectedApp from './containers/ConnectedApp'
import store from './configureStore'
  1. 最后,通过将ConnectedApp组件与Provider组件包装起来,调整ReactDOM.render的第一个参数,如下所示:
ReactDOM.render(
 <Provider store={store}>
 <ConnectedApp />
 </Provider>,
    document.getElementById('root')
)

现在,我们的应用程序将以与以前相同的方式工作,但一切都连接到 Redux 存储!正如我们所看到的,Redux 需要比仅仅使用 React 更多的样板代码,但它带来了许多优势:

  • 更容易处理异步操作(使用redux-thunk中间件)

  • 集中的操作处理(无需在组件中定义操作创建者)

  • 用于绑定操作创建者和组合减速器的有用函数

  • 减少错误的可能性(例如,通过使用操作类型,我们可以确保没有拼写错误)

然而,也有以下缺点:

  • 需要大量的样板代码(操作类型、操作创建者和连接组件)

  • 在单独的文件中映射状态/操作创建者(而不是在需要它们的组件中)

第一点既是优点又是缺点;操作类型和操作创建者确实需要更多的样板代码,但它们也使得以后更容易更新与操作相关的代码。第二点,以及连接组件所需的样板代码,可以通过使用 Hooks 来连接我们的组件到 Redux 来解决。我们将在本章的下一节中使用 Redux 与 Hooks。

示例代码

示例代码可以在Chapter12/chapter12_2文件夹中找到。

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

使用 Hooks 与 Redux

将我们的待办事项应用程序转换为基于 Redux 的应用程序后,我们现在使用高阶组件而不是 Hooks 来访问 Redux 状态和动作创建者。这是开发 Redux 应用程序的传统方式。但是,在 Redux 的最新版本中,可以使用 Hooks 代替高阶组件!我们现在将用 Hooks 替换现有的连接器。

即使使用了 Hooks,Provider组件仍然需要,以便向其他组件提供 Redux 存储。当从connect()重构到 Hooks 时,存储和提供程序的定义可以保持不变。

最新版本的 React Redux 提供了各种 Hooks 作为connect()高阶组件的替代方案。使用这些 Hooks,您可以订阅 Redux 存储,并且无需包装组件即可分派动作。

使用 dispatch Hook

useDispatch Hook 返回 Redux 存储提供的dispatch函数的引用。它可以用于分派从动作创建者返回的动作。其 API 如下所示:

const dispatch = useDispatch()

现在,我们将使用 Dispatch Hook 来替换现有的容器组件。

您不需要一次性迁移整个 Redux 应用程序以使用 Hooks。可以有选择性地重构某些组件,这意味着它们将使用 Hooks,同时仍然使用connect()来处理其他组件。

学会如何使用 Dispatch Hook 后,让我们继续迁移现有组件,使它们使用 Dispatch Hook。

使用 Hooks 为 AddTodo 组件

现在我们已经了解了 Dispatch Hook,让我们通过在AddTodo组件中实现它来看看它的作用。

现在让我们将AddTodo组件迁移到 Hooks:

  1. 首先删除src/containers/ConnectedAddTodo.js文件。

  2. 现在,编辑src/components/AddTodo.js文件并从react-redux中导入useDispatch Hook:

import { useDispatch } from 'react-redux'
  1. 另外,导入addTodo动作创建者:
import { addTodo } from '../actions'
  1. 现在,我们可以从函数定义中删除 props:
export default function AddTodo () {
  1. 然后,定义 Dispatch Hook:
    const dispatch = useDispatch()
  1. 最后,调整处理程序函数并调用dispatch()
    function handleAdd () {
        if (input) {
            dispatch(addTodo(input))
            setInput('')
        }
    }
  1. 现在,唯一剩下的事情就是在src/components/App.js中用AddTodo组件替换ConnectedAddTodo组件。首先,调整导入语句:
import AddTodo from './AddTodo'
  1. 然后,调整渲染的组件:
    return (
        <div style={{ width: 400 }}>
            <Header />
            <AddTodo />

如您所见,我们的应用程序仍然以与以前相同的方式工作,但现在我们正在使用 Hooks 来连接组件到 Redux!

使用 Hooks 为 App 组件

接下来,我们将更新我们的App组件,以便直接调度fetchTodos动作。现在让我们将App组件迁移到 Hooks:

  1. 首先删除src/containers/ConnectedApp.js文件。

  2. 现在,编辑src/components/App.js文件,并从react-redux导入useDispatch Hook:

import { useDispatch } from 'react-redux'
  1. 另外,导入fetchTodos动作创建者:
import { fetchTodos } from '../actions'
  1. 现在,我们可以从函数定义中删除 props:
export default function App () {
  1. 然后,定义 Dispatch Hook:
    const dispatch = useDispatch()
  1. 最后,调整 Effect Hook 并调用dispatch()
    useEffect(() => {
        dispatch(fetchTodos())
    }, [ dispatch ])
  1. 现在,剩下的就是在src/index.js中用App组件替换ConnectedApp组件。首先,调整导入语句:
import App from './components/App'
  1. 然后,调整渲染的组件:
ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

正如我们所看到的,使用 Hooks 比定义一个单独的容器组件更简单和更简洁。

使用 Hooks 为 TodoItem 组件

现在,我们将升级TodoItem组件以使用 Hooks。让我们现在迁移它:

  1. 首先删除src/containers/ConnectedTodoItem.js文件。

  2. 现在,编辑src/components/TodoItem.js文件,并从react-redux导入useDispatch Hook:

import { useDispatch } from 'react-redux'
  1. 另外,导入toggleTodoremoveTodo动作创建者:
import { toggleTodo, removeTodo } from '../actions'
  1. 现在,我们可以从函数定义中删除与动作创建者相关的 props。新代码应如下所示:
export default function TodoItem ({ title, completed, id }) {
  1. 然后,定义 Dispatch Hook:
    const dispatch = useDispatch()
  1. 最后,调整处理函数以调用dispatch()
    function handleToggle () {
        dispatch(toggleTodo(id))
    }

    function handleRemove () {
        dispatch(removeTodo(id))
    }
  1. 现在,剩下的就是在src/components/TodoList.js中用TodoItem组件替换ConnectedTodoItem组件。首先,调整导入语句:
import TodoItem from './TodoItem'
  1. 然后,调整渲染的组件:
    return filteredTodos.map(item =>
        <TodoItem {...item} key={item.id} />
    )

现在,TodoItem组件使用 Hooks 而不是容器组件。接下来,我们将学习有关 Selector Hook 的内容。

使用 Selector Hook

Redux 提供的另一个非常重要的 Hook 是 Selector Hook。它允许我们通过定义选择器函数从 Redux 存储状态中获取数据。该 Hook 的 API 如下:

const result = useSelector(selectorFn, equalityFn)

selectorFn是一个类似于mapStateToProps函数的函数。它将完整的状态对象作为唯一参数。当组件渲染时,选择器函数将被执行,以及当动作被调度时(并且状态与先前状态不同)。

重要的是要注意,从一个选择器 Hook 返回多个状态部分的对象将在每次分派动作时强制重新渲染。如果需要请求存储中的多个值,我们可以这样做:

  • 使用多个 Selector Hooks,每个返回状态对象中的单个字段

  • 使用reselect或类似的库创建一个记忆化选择器(我们将在下一节中介绍)

  • 使用react-redux中的shallowEqual函数作为equalityFn

现在,我们将在我们的 ToDo 应用程序中实现选择器 Hook,特别是在TodoListTodoFilter组件中。

使用 Hooks 为 TodoList 组件

首先,我们将实现一个选择器 Hook 来获取TodoList组件的所有todos,如下所示:

  1. 首先删除src/containers/ConnectedTodoList.js文件。

  2. 现在,编辑src/components/TodoList.js文件,并从react-redux中导入useSelector Hook:

import { useSelector } from 'react-redux'
  1. 现在,我们可以从函数定义中删除所有的 props:
export default function TodoList () {
  1. 然后,我们定义两个 Selector Hooks,一个用于filter值,一个用于todos值:
    const filter = useSelector(state => state.filter)
    const todos = useSelector(state => state.todos)
  1. 现在,剩下的就是在src/components/App.js中用TodoList组件替换ConnectedTodoList组件。首先,调整导入语句:
import TodoList from './TodoList'
  1. 然后,调整渲染的组件:
    return (
        <div style={{ width: 400 }}>
            <Header />
            <AddTodo />
            <hr />
            <TodoList />

组件的其余部分可以保持不变,因为我们存储状态部分的值与以前的名称相同。

使用 Hooks 为 TodoFilter 组件

最后,我们将在TodoFilter组件中实现选择器和 Dispatch Hooks,因为我们需要突出显示当前的过滤器(来自选择器 Hook 的状态)并分派一个动作来改变过滤器(Dispatch Hook)。

现在让我们为TodoFilter组件实现 Hooks:

  1. 首先,删除src/containers/ConnectedTodoFilter.js文件。

  2. 我们还可以删除src/containers/文件夹,因为现在它是空的。

  3. 现在,编辑src/components/TodoFilter.js文件,并从react-redux中导入useSelectoruseDispatch Hooks:

import { useSelector, useDispatch } from 'react-redux'
  1. 另外,导入filterTodos动作创建者:
import { filterTodos } from '../actions'
  1. 现在,我们可以从函数定义中删除所有的 props:
export default function TodoFilter () {
  1. 然后,定义 Dispatch 和 Selector Hooks:
    const dispatch = useDispatch()
    const filter = useSelector(state => state.filter)
  1. 最后,调整处理函数以调用dispatch()
    function handleFilter () {
        dispatch(filterTodos(name))
    }
  1. 现在,剩下的就是在src/components/App.js中用TodoFilter组件替换ConnectedTodoFilter组件。首先,调整导入语句:
import TodoFilter from './TodoFilter'
  1. 然后,调整渲染的组件:
    return (
        <div style={{ width: 400 }}>
            <Header />
            <AddTodo />
            <hr />
            <TodoList />
            <hr />
            <TodoFilter />
        </div>
    )

现在,我们的 Redux 应用程序完全使用 Hooks 而不是容器组件!

示例代码

示例代码可以在Chapter12/chapter12_3文件夹中找到。

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

创建可重用的选择器

在定义选择器时,我们一直以来都是在每次组件渲染时创建选择器的新实例。这是可以的,如果选择器函数不执行任何复杂操作并且不维护内部状态。否则,我们需要使用可重用的选择器,现在我们将学习有关它们的知识。

设置 reselect

为了创建可重用的选择器,我们可以使用reselect库中的createSelector函数。首先,我们必须通过npm安装该库。执行以下命令:

> npm install --save reselect

现在,reselect库已经安装,我们可以使用它来创建可重用的选择器。

对仅依赖于状态的选择器进行记忆化

如果我们想要记忆化选择器,并且选择器仅依赖于状态(而不是 props),我们可以在组件外部声明选择器,如下所示:

  1. 编辑src/components/TodoList.js文件,并从reselect中导入createSelector函数:
import { createSelector } from 'reselect'
  1. 然后,在组件定义之前,我们为状态的todosfilter部分定义选择器:
const todosSelector = state => state.todos
const filterSelector = state => state.filter

如果选择器被许多组件使用,将它们放在单独的selectors.js文件中并从那里导入可能是有意义的。例如,我们可以将filterSelector放在一个单独的文件中,然后在TodoList.js以及TodoFilter.js中导入它。

  1. 现在,在定义组件之前,我们为过滤后的 todos 定义一个选择器,如下所示:
const selectFilteredTodos = createSelector(
  1. 首先,我们指定要重用的另外两个选择器:
    todosSelector,
    filterSelector,
  1. 现在,我们指定一个过滤选择器,从useMemo Hook 中复制代码:
    (todos, filter) => {
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }
)
  1. 最后,在选择器钩子中使用我们定义的选择器:
export default function TodoList () {
    const filteredTodos = useSelector(selectFilteredTodos)

现在我们已经为过滤后的 todos 定义了一个可重用的选择器,过滤 todos 的结果将被记忆化,如果状态没有改变,将不会重新计算。

示例代码

示例代码可以在Chapter12/chapter12_4文件夹中找到。

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

使用 store Hook

React Redux 还提供了一个useStore Hook,它返回对 Redux 存储本身的引用。这是传递给Provider组件的相同store对象。其 API 如下:

const store = useStore()

最好的做法是避免直接使用 Store Hook。通常更有意义的是使用 Dispatch 或 Selector Hooks。但是,也有特殊情况,比如替换 reducer,可能需要使用这个 Hook。

在本节中,我们已经学会了如何在现有的 Redux 应用程序中用 Hooks 替换连接器。现在,我们将学习一种策略,可以有效地将现有的 Redux 应用程序迁移到 Hooks。

迁移 Redux 应用程序

在一些 Redux 应用程序中,本地状态也存储在 Redux 状态树中。在其他情况下,React 类组件状态用于存储本地状态。在任何情况下,迁移现有的 Redux 应用程序的方法如下:

  • 用状态钩子替换简单的本地状态,比如输入字段的值

  • 用 Reducer Hooks 替换复杂的本地状态

  • 全局状态(在多个组件中使用的状态)存储在 Redux 存储中

在上一章中,我们已经学会了如何迁移 React 类组件。在上一节中,我们学会了如何从 Redux 连接器迁移到使用 Selector 和 Dispatch Hooks。现在,我们将展示一个将 Redux 本地状态迁移到基于 Hook 的方法的示例。

假设我们现有的待办事项应用程序将输入字段状态存储在 Redux 中,如下所示:

{
    "todos": [],
    "filter": "all",
    "newTodo": ""
}

现在,每当输入文本时,我们需要分派一个动作,通过调用所有的 reducer 来计算新的状态,然后更新 Redux 存储状态。可以想象,如果有很多输入字段,这可能会导致性能问题。 我们应该使用状态钩子来存储这个本地状态,因为它只在一个组件内部使用。在我们的示例应用程序中,我们在实现AddTodo组件时已经正确地做到了这一点。

现在我们已经学会了如何将现有的 Redux 应用程序迁移到 Hooks,我们可以继续讨论 Redux 的权衡。

Redux 的权衡

总结一下,让我们总结一下在 Web 应用程序中使用 Redux 的利弊。首先,让我们从积极的方面开始:

  • 提供了一定的项目结构,使我们可以轻松地扩展和修改代码

  • 我们的代码中出错的可能性较少

  • 比使用 React Context 进行状态管理性能更好

  • 使 App 组件更简单(将状态管理和操作创建者卸载到 Redux)

Redux 是处理复杂状态变化和在许多组件中使用的状态的较大项目的完美选择。

但是,使用 Redux 也有缺点:

  • 需要编写样板代码

  • 项目结构变得更加复杂

  • Redux 需要一个包装组件(Provider)来连接应用程序到存储

因此,Redux 不应该用于简单的项目。在这些情况下,Reducer Hook 可能就足够了。使用 Reducer Hook,我们无需包装组件来连接我们的应用程序到状态存储。此外,如果我们使用多个 Reducer Hook,向特定的 reducer 发送操作稍微更有效,而不是全局应用程序 reducer。然而,缺点在于必须处理多个 dispatch 函数,并保持各种状态同步。我们也不能使用中间件,包括对 Reducer Hook 的异步操作的支持。如果状态变化复杂但仅局限于某个组件,可能使用 Reducer Hook 是有意义的,但如果状态在多个组件中使用,或者对整个应用程序都很重要,我们应该将其存储在 Redux 中。

如果您的组件不执行以下操作,则可能不需要 Redux:

  • 使用网络

  • 保存或加载状态

  • 与其他非子组件共享状态

在这种情况下,使用状态或 Reducer Hook 而不是 Redux 是有意义的。

总结

在本章中,我们首先学习了 Redux 是什么,以及何时以及为什么应该使用它。然后,我们学习了 Redux 的三个原则。接下来,我们在实践中使用 Redux 处理我们的 ToDo 应用程序中的状态。我们还学习了同步和异步操作创建者。然后,我们学习了如何使用 Hooks 使用 Redux,以及如何将现有的 Redux 应用程序迁移到基于 Hook 的解决方案。最后,我们了解了使用 Redux 和 Reducer Hook 的权衡。

在下一章中,我们将学习如何使用 MobX 处理状态。我们将学习 MobX 是什么,以及如何以传统方式与 React 一起使用它。然后,我们将学习如何使用 Hooks 使用 MobX,并且我们还将了解如何将现有的 MobX 应用程序迁移到基于 Hook 的解决方案。

问题

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

  1. Redux 应该用于哪种状态?

  2. Redux 由哪些元素组成?

  3. Redux 的三大原则是什么?

  4. 为什么我们要定义动作类型?

  5. 我们如何将组件连接到 Redux?

  6. 我们可以使用哪些 Hooks 与 Redux?

  7. 为什么我们应该创建可重用的选择器?

  8. 我们如何迁移 Redux 应用程序?

  9. Redux 的权衡是什么?

  10. 我们什么时候应该使用 Redux?

进一步阅读

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

第十三章:MobX 和 Hooks

在上一章中,我们学习了 Redux 以及如何将 Redux 与 Hooks 结合使用。我们还学习了如何将现有的 Redux 应用迁移到基于 Hook 的解决方案。此外,我们还了解了使用 Reducer Hooks 与 Redux 的权衡,以及何时使用其中之一。

在本章中,我们将学习如何将 MobX 与 Hooks 结合使用。我们将首先学习如何使用 MobX 处理状态,然后转而使用 Hooks 与 MobX。此外,我们还将学习如何将现有的 MobX 应用迁移到 Hooks。最后,我们将讨论使用 MobX 的利弊。通过本章的学习,您将完全了解如何使用 Hooks 编写 MobX 应用程序。

本章将涵盖以下主题:

  • 了解 MobX 是什么以及它是如何工作的

  • 使用 MobX 处理状态

  • 使用 Hooks 与 MobX

  • 迁移 MobX 应用

  • 了解 MobX 的权衡

技术要求

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

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

查看以下视频以查看代码的实际操作:

bit.ly/2Mm9yoC

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

现在,让我们开始本章。

什么是 MobX?

MobX 采用了与 Redux 不同的方法。它不是施加限制以使状态变化可预测,而是旨在自动更新从应用程序状态派生的任何内容。与分派动作不同,在 MobX 中,我们可以直接修改状态对象,MobX 将负责更新使用状态的任何内容。

MobX 的生命周期如下:

  1. 事件(如onClick)调用动作,这是唯一可以修改状态的东西:
@action onClick = () => {
    this.props.todo.completed = true
}
  1. 状态是可观察的,不应包含冗余或可推导的数据。状态非常灵活 - 它可以包含类、数组、引用,甚至可以是图:
@observable todos = [
    { title: 'Learn MobX', completed: false }
]
  1. 计算值是通过纯函数从状态派生出来的。这些将被 MobX 自动更新:
@computed get activeTodos () {
    return this.todos.filter(todo => !todo.completed)
}
  1. 反应就像计算值,但它们也可以产生副作用,而不是一个值,比如在 React 中更新用户界面:
const TodoList = observer(({ todos }) => (
    <div>
        {todos.map(todo => <TodoItem {...todo} />)}
    </div>
)

我们可以在以下图表中看到 MobX 的生命周期可视化:

MobX 生命周期的可视化

MobX 和 React 非常搭配。每当 MobX 检测到状态已经改变,它将导致适当的组件重新渲染。

与 Redux 不同,使用 MobX 并不需要学习很多限制。我们只需要了解一些核心概念,比如可观察值、计算值和反应。

现在我们知道了 MobX 的生命周期,让我们继续实践中使用 MobX 处理状态。

使用 MobX 处理状态

了解 MobX 最好的方法是在实践中使用它并看看它是如何工作的。所以,让我们从第十一章的 ToDo 应用程序开始,从 React 类组件迁移到 MobX。我们首先要做的是从Chapter11/chapter11_2/复制代码示例。

安装 MobX

第一步是通过npm安装 MobX 和 MobX React。执行以下命令:

> npm install --save mobx mobx-react

现在 MobX 和 MobX React 都安装好了,我们可以开始设置存储了。

设置 MobX 存储

安装完 MobX 后,现在是时候设置我们的 MobX 存储了。存储将存储所有状态,以及相关的计算值和操作。通常使用类来定义。

现在让我们定义 MobX 存储:

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

  2. 从 MobX 导入observableactioncomputed装饰器,以及decorate函数。这些将用于标记存储中的各种函数和值:

import { observable, action, computed, decorate } from 'mobx'
  1. 还要从我们的 API 代码中导入fetchAPITodosgenerateID函数:
import { fetchAPITodos, generateID } from './api'
  1. 现在,我们通过使用一个类来定义存储:
export default class TodoStore {
  1. 在这个存储中,我们存储了一个todos数组和filter字符串值。这两个值都是可观察的。我们将在稍后将它们标记为这样:
    todos = []
    filter = 'all'

通过特殊的项目设置,我们可以使用一个实验性的 JavaScript 特性,称为装饰器,通过编写@observable todos = []来将我们的值标记为可观察的。然而,这种语法不受create-react-app支持,因为它还不是 JavaScript 标准的一部分。

  1. 接下来,我们定义一个计算值,以便从我们的 store 中获取所有经过过滤的todos。该函数将类似于我们在src/App.js中的函数,但现在我们将使用this.filterthis.todos。同样,我们必须稍后将该函数标记为computed。MobX 将在需要时自动触发此函数,并存储结果,直到它所依赖的状态发生变化。
    get filteredTodos () {
        switch (this.filter) {
            case 'active':
                return this.todos.filter(t => t.completed === false)

            case 'completed':
                return this.todos.filter(t => t.completed === true)

            default:
            case 'all':
                return this.todos
        }
    }
  1. 现在,我们定义我们的动作。我们从fetch动作开始。与以前一样,我们必须稍后使用action装饰器标记我们的动作函数。在 MobX 中,我们可以通过设置this.todos直接修改我们的状态。因为todos值是可观察的,对它的任何更改都将被 MobX 自动跟踪:
    fetch () {
        fetchAPITodos().then((fetchedTodos) => {
            this.todos = fetchedTodos
        })
    }
  1. 然后,我们定义了addTodo动作。在 MobX 中,我们不使用不可变的值,因此不应创建新数组。相反,我们总是修改现有的this.todos值:
    addTodo (title) {
        this.todos.push({ id: generateID(), title, completed: false })
    }

正如您所看到的,MobX 采用更加命令式的方法,直接修改值,MobX 会自动跟踪更改。我们不需要使用 rest/spread 语法来创建新数组;相反,我们直接修改现有状态数组。

  1. 接下来是toggleTodo动作。在这里,我们循环遍历所有的todos并修改具有匹配id的项目。请注意,我们可以修改数组中的项目,并且更改仍将被 MobX 跟踪。事实上,MobX 甚至会注意到数组中只有一个值已经改变。结合 React,这意味着列表组件不会重新渲染;只有更改的项目组件将重新渲染。请注意,为了实现这一点,我们必须适当地拆分组件,例如制作单独的列表和项目组件:
    toggleTodo (id) {
        for (let todo of this.todos) {
            if (todo.id === id) {
                todo.completed = !todo.completed
                break
            }
        }
    }

for (let .. of ..) {结构将循环遍历数组的所有项目,或任何其他可迭代的值。

  1. 现在,我们定义了removeTodo动作。首先,我们找到要删除的todo项目的index
    removeTodo (id) {
        let index = 0
        for (let todo of this.todos) {
            if (todo.id === id) {
                break
            } else {
                index++
            }
        }
  1. 然后,我们使用splice来删除一个元素——从找到的元素的index开始。这意味着我们从数组中剪切具有给定id的项目:
        this.todos.splice(index, 1)
    }
  1. 我们定义的最后一个动作是filterTodos动作。在这里,我们只需将this.filter值设置为新的过滤器:
    filterTodos (filterName) {
        this.filter = filterName
    }
}
  1. 最后,我们必须使用我们之前提到的各种装饰器来装饰我们的 store。我们通过在我们的 store 类上调用decorate函数并传递一个将值和方法映射到装饰器的对象来实现这一点:
decorate(TodoStore, {
  1. 我们从可观察的todosfilter值开始:
    todos: observable,
    filter: observable,
  1. 然后,我们装饰computedfilteredTodos
    filteredTodos: computed,
  1. 最后但并非最不重要的是,我们装饰我们的动作:
    fetch: action,
    addTodo: action,
    toggleTodo: action,
    removeTodo: action,
    filterTodos: action
})

现在,我们的 MobX 存储已经正确装饰并准备好使用!

定义 Provider 组件

现在我们可以在App组件中初始化存储,并将其传递给所有其他组件。然而,更好的做法是使用 React Context。这样,我们可以在应用程序的任何地方访问存储。MobX React 提供了一个Provider组件,它在上下文中提供存储。

现在让我们开始使用Provider组件:

  1. 编辑src/index.js,并从mobx-react中导入Provider组件:
import { Provider } from 'mobx-react'
  1. 然后,从我们的store.js文件中导入TodoStore
import TodoStore from './store'
  1. 现在,我们创建TodoStore类的一个新实例:
const store = new TodoStore()
  1. 最后,我们必须调整ReactDOM.render()的第一个参数,以便用Provider组件包装App组件:
ReactDOM.render(
    <Provider todoStore={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

与 Redux 不同,使用 MobX 可以在我们的应用程序中提供多个存储。然而,在这里,我们只提供一个存储,并将其称为todoStore

现在,我们的存储已经初始化并准备在所有其他组件中使用。

连接组件

现在我们的 MobX 存储作为上下文可用,我们可以开始将我们的组件连接到它。为此,MobX React 提供了inject高阶组件,我们可以用它将存储注入到我们的组件中。

在这一部分,我们将把以下组件连接到我们的 MobX 存储中:

  • App

  • TodoList

  • TodoItem

  • AddTodo

  • TodoFilter

连接 App 组件

我们将从连接我们的App组件开始,在那里我们将使用fetch动作从我们的 API 中获取所有todos

现在让我们连接App组件:

  1. 编辑src/App.js,并从mobx-react中导入inject函数:
import { inject } from 'mobx-react'
  1. 然后,用inject包装App组件。inject函数用于将存储(或多个存储)作为 props 注入到组件中:
export default inject('todoStore')(function App ({ todoStore }) {

inject函数中可以指定多个存储,如下所示:inject('todoStore', 'otherStore')。然后,将注入两个 props:todoStoreotherStore

  1. 现在我们有了todoStore,我们可以在 Effect Hook 中调用fetch动作:
    useEffect(() => {
 todoStore.fetch()
    }, [ todoStore ])
  1. 现在,我们可以删除filteredTodos Memo Hook、处理函数、StateContext.Provider组件以及我们传递给其他组件的所有 props:
    return (
        <div style={{ width: 400 }}>
            <Header />
            <AddTodo />
            <hr />
            <TodoList />
            <hr />
            <TodoFilter />
        </div>
    )
})

现在,我们的App组件将从 API 获取todos,然后它们将被存储在TodoStore中。

连接 TodoList 组件

在将todos存储在我们的存储中后,我们可以从存储中获取它们,然后我们可以在TodoList组件中列出所有的待办事项。

现在让我们连接TodoList组件:

  1. 编辑src/TodoList.js并导入injectobserver函数:
import { inject, observer } from 'mobx-react'
  1. 删除所有与上下文相关的导入和 Hooks。

  2. 与以前一样,我们使用inject函数来包装组件。此外,我们现在用observer函数包装我们的组件。observer函数告诉 MobX,当存储更新时,这个组件应该重新渲染:

export default inject('todoStore')(observer(function TodoList ({ todoStore }) {
  1. 我们现在可以使用存储中的filteredTodos计算值,以列出所有应用了过滤器的待办事项。为了确保 MobX 仍然可以跟踪item对象发生的更改,我们在这里使用扩展语法。如果我们使用了扩展语法,所有的待办事项都会重新渲染,即使只有一个发生了变化:
    return todoStore.filteredTodos.map(item =>
        <TodoItem key={item.id} item={item} />
    )
}))

现在,我们的应用程序已经列出了所有的待办事项。但是,我们还不能切换或删除待办事项。

连接 TodoItem 组件

为了能够切换或删除待办事项,我们必须连接TodoItem组件。我们还将TodoItem组件定义为观察者,以便 MobX 知道它将在item对象更改时重新渲染组件。

现在让我们连接TodoItem组件:

  1. 编辑src/TodoItem.js,并从mobx-react中导入injectobserver函数:
import { inject, observer } from 'mobx-react'
  1. 然后,用injectobserver包装TodoItem组件:
export default inject('todoStore')(observer(function TodoItem ({ item, todoStore }) {
  1. 我们现在可以在组件内部使用item对象的解构。由于它被定义为观察者,MobX 将能够在解构后跟踪item对象的更改:
    const { title, completed, id } = item
  1. 现在我们有了todoStore,我们可以使用它来调整我们的处理函数,并调用相应的动作:
    function handleToggle () {
        todoStore.toggleTodo(id)
    }

    function handleRemove () {
        todoStore.removeTodo(id)
    }

现在,我们的TodoItem组件将调用todoStore中的toggleTodoremoveTodo动作,所以我们现在可以切换和删除待办事项!

连接 AddTodo 组件

为了能够添加新的待办事项,我们必须连接AddTodo组件。

现在让我们连接AddTodo组件:

  1. 编辑src/AddTodo.js并从mobx-react中导入inject函数:
import { inject } from 'mobx-react'
  1. 然后,用inject包装AddTodo组件:
export default inject('todoStore')(function AddTodo ({ todoStore }) {
  1. 现在我们有了todoStore,我们可以使用它来调整我们的处理函数,并调用addTodo动作:
    function handleAdd () {
        if (input) {
            todoStore.addTodo(input)
            setInput('')
        }
    }

现在,我们的AddTodo组件将调用我们的todoStore中的addTodo动作,所以我们现在可以添加新的待办事项!

连接 TodoFilter 组件

最后,我们必须连接TodoFilter组件,以便能够选择不同的过滤器。我们还希望显示当前选定的过滤器,因此这个组件需要是一个observer

让我们现在连接TodoFilter组件:

  1. 编辑src/TodoFilter.js并导入injectobserver函数:
import { inject, observer } from 'mobx-react'
  1. 我们使用injectobserver函数来包装组件:
const TodoFilterItem = inject('todoStore')(observer(function TodoFilterItemWrapped ({ name, todoStore }) {
  1. 现在我们调整我们的处理函数,以调用存储中的filterTodos动作:
    function handleFilter () {
        todoStore.filterTodos(name)
    }
  1. 最后,我们调整style对象,以使用todoStore中的filter值,以检查过滤器当前是否被选中:
    const style = {
        color: 'blue',
        cursor: 'pointer',
        fontWeight: (todoStore.filter === name) ? 'bold': 'normal'
    }
  1. 此外,我们现在可以摆脱在FilterItem组件中传递 props。删除以下用粗体标记的部分:
export  default  function  TodoFilter  (props)  {
  return ( <div> <TodoFilterItem {...props} name="all" />{' / '} <TodoFilterItem {...props} name="active" />{' / '} <TodoFilterItem {...props} name="completed" /> </div> ) }

现在,我们可以选择新的过滤器,它们将被标记为选定,并以粗体显示。待办事项列表也将自动过滤,因为 MobX 检测到filter值的变化,导致filteredTodos计算值更新,并且TodoList观察者组件重新渲染。

示例代码

示例代码可以在Chapter13/chapter13_1文件夹中找到。

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

使用 MobX 和 Hooks

在上一节中,我们学习了如何在 React 中使用 MobX。正如我们所见,为了能够将我们的组件连接到 MobX 存储,我们需要使用inject函数将它们包装起来,并且在某些情况下,还需要使用observer函数。自从mobx-react的 v6 版本发布以来,我们可以使用 Hooks 来连接我们的组件到 MobX 存储,而不是使用这些高阶组件来包装我们的组件。我们现在要使用 MobX 和 Hooks!

定义一个存储 Hook

首先,我们必须定义一个 Hook 以便访问我们自己的存储。正如我们之前学到的,MobX 使用 React Context 来提供和注入状态到各种组件中。我们可以从mobx-react中获取MobXProviderContext并创建我们自己的自定义上下文 Hook 以便访问所有存储。然后,我们可以创建另一个 Hook,专门访问我们的TodoStore

所以,让我们开始定义一个存储 Hook:

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

  2. react中导入useContext Hook,以及从mobx-react中导入MobXProviderContext

import { useContext } from 'react'
import { MobXProviderContext } from 'mobx-react'
  1. 现在,我们定义并导出一个useStores Hook,它返回一个用于MobXProviderContext的 Context Hook:
export function useStores () {
    return useContext(MobXProviderContext)
}
  1. 最后,我们定义一个useTodoStore Hook,它从我们之前的 Hook 中获取todoStore,然后返回它:
export function useTodoStore () {
    const { todoStore } = useStores()
    return todoStore
}

现在,我们有一个通用的 Hook,可以访问 MobX 的所有 stores,以及一个特定的 Hook 来访问TodoStore。如果需要的话,我们也可以在以后定义更多的 Hooks 来访问其他 stores。

升级组件到 Hooks

创建一个 Hook 来访问我们的 store 后,我们可以使用它来代替用inject高阶组件函数包装我们的组件。在接下来的部分中,我们将看到如何使用 Hooks 来升级我们的各种组件。

为 App 组件使用 Hooks

我们将从升级我们的App组件开始。逐渐重构组件,使其使用 Hooks 是可能的。我们不需要一次性重构每个组件。

现在让我们为App组件使用 Hooks:

  1. 编辑src/App.js并删除以下import语句:
import { inject } from 'mobx-react'
  1. 然后,从我们的hooks.js文件中导入useTodoStore Hook:
import { useTodoStore } from './hooks'
  1. 现在,删除包装App组件的inject函数,并删除所有 props。App函数定义现在应该如下所示:
export default function App () {
  1. 最后,使用我们的 Todo Store Hook 来获取todoStore对象:
    const todoStore = useTodoStore()

如你所见,我们的应用程序仍然以与以前相同的方式工作!然而,我们现在在App组件中使用 Hooks,这使得代码更加清晰和简洁。

TodoList组件使用 Hooks

接下来,我们将升级我们的TodoList组件。此外,我们还将使用useObserver Hook,它替换了observer高阶组件。

现在让我们为TodoList组件使用 Hooks:

  1. 编辑src/TodoList.js,并删除以下导入语句:
import { inject, observer } from 'mobx-react'
  1. 然后,从mobx-react中导入useObserver Hook,以及从我们的hooks.js文件中导入useTodoStore Hook:
import { useObserver } from 'mobx-react'
import { useTodoStore } from './hooks'
  1. 现在,删除包装TodoList组件的injectobserver函数,并且也删除所有 props。TodoList函数定义现在应该如下所示:
export default function TodoList () {
  1. 再次,我们使用 Todo Store Hook 来获取todoStore对象:
    const todoStore = useTodoStore()
  1. 最后,我们用useObserver Hook 包装返回的元素。在 Observer Hook 中的所有内容将在 Hook 中使用的状态发生变化时重新计算:
    return useObserver(() =>
        todoStore.filteredTodos.map(item =>
            <TodoItem key={item.id} item={item} />
        )
    )
}

在我们的情况下,MobX 将检测到通过useObserver Hook 定义的观察者依赖于todoStore.filteredTodos,而filteredTodos依赖于filtertodos值。因此,每当filter值或todos数组发生更改时,列表将重新渲染。

TodoItem组件使用 Hooks

接下来,我们将升级TodoItem组件,这将是与TodoList组件相似的过程。

现在让我们为TodoItem组件使用 Hooks:

  1. 编辑src/TodoItem.js并删除以下import语句:
import { inject, observer } from 'mobx-react'
  1. 然后,从mobx-react中导入useObserver Hook,从我们的hooks.js文件中导入useTodoStore Hook:
import { useObserver } from 'mobx-react'

import { useTodoStore } from './hooks'
  1. 现在,删除包裹TodoItem组件的injectobserver函数,也删除todoStore属性。TodoItem函数定义现在应该如下所示:
export default function TodoItem ({ item }) {
  1. 接下来,我们必须删除解构(粗体代码),因为我们整个组件不再被定义为可观察的,因此 MobX 将无法跟踪item对象的更改:
 const { title, completed, id } = item
  1. 然后,使用 Todo Store Hook 来获取todoStore对象:
    const todoStore = useTodoStore()
  1. 现在,我们必须调整处理函数,使其直接使用item.id而不是id。请注意,我们假设id不会改变,因此它不会被包裹在 Observer Hook 中:
    function handleToggle () {
        todoStore.toggleTodo(item.id)
    }

    function handleRemove () {
        todoStore.removeTodo(item.id)
    }
  1. 最后,我们用 Observer Hook 包裹return语句并在那里进行解构。这确保了 MobX 会跟踪item对象的更改,并且当对象的属性更改时,组件将相应地重新渲染:
    return useObserver(() => {
 const { title, completed } = item
        return (
            <div style={{ width: 400, height: 25 }}>
                <input type="checkbox" checked={completed} onChange={handleToggle} />
                {title}
                <button style={{ float: 'right' }} onClick={handleRemove}>x</button>
            </div>
        )
    })
}

现在,我们的TodoItem组件已经正确连接到 MobX 存储。

如果item.id属性发生更改,我们将不得不将处理函数和return函数包裹在单个useObserver Hook 中,如下所示:

    return useObserver(() => {
        const { title, completed, id } = item

        function handleToggle () {
            todoStore.toggleTodo(id)
        }

        function handleRemove () {
            todoStore.removeTodo(id)
        }

        return (
            <div style={{ width: 400, height: 25 }}>
                <input type="checkbox" checked={completed} onChange={handleToggle} />
                {title}
                <button style={{ float: 'right' }} onClick={handleRemove}>x</button>
            </div>
        )
    })

请注意,我们不能将处理函数和return语句分别包裹在单独的 Observer Hooks 中,因为这样处理函数将只在第一个 Observer Hook 的闭包内定义。这意味着我们将无法从第二个 Observer Hook 内访问处理函数。

接下来,我们将继续通过使用 Hooks 来升级AddTodo组件的组件。

AddTodo组件使用 Hooks

我们重复与App组件中相同的升级过程,用于AddTodo组件,如下所示:

  1. 编辑src/AddTodo.js并删除以下import语句:
import { inject } from 'mobx-react'
  1. 然后,从我们的hooks.js文件中导入useTodoStore Hook:
import { useTodoStore } from './hooks'
  1. 现在,删除包装AddTodo组件的inject函数,也删除所有 props。AddTodo函数定义现在应该如下所示:
export default function AddTodo () {
  1. 最后,使用 Todo Store Hook 来获取todoStore对象:
    const todoStore = useTodoStore()

现在,我们的AddTodo组件已连接到 MobX 存储,我们可以继续升级TodoFilter组件。

使用 Hooks 来处理TodoFilter组件

对于TodoFilter组件,我们将使用类似于我们用于TodoList组件的过程。我们将使用我们的useTodoStore Hook 和useObserver Hook。

现在让我们为TodoFilter组件使用 Hooks:

  1. 编辑src/TodoFilter.js并删除以下import语句:
import { inject, observer } from 'mobx-react'
  1. 然后,从mobx-react导入useObserver Hook,以及从我们的hooks.js文件中导入useTodoStore Hook:
import { useObserver } from 'mobx-react'
import { useTodoStore } from './hooks'
  1. 现在,删除包装TodoFilterItem组件的injectobserver函数,也删除todoStore prop。TodoFilterItem函数定义现在应该如下所示:
function TodoFilterItem ({ name }) {
  1. 再次,我们使用 Todo Store Hook 来获取todoStore对象:
    const todoStore = useTodoStore()
  1. 最后,我们使用useObserver Hook 将style对象包装起来。请记住,Observer Hook 内的所有内容都将在 Hook 中使用的状态发生变化时重新计算:
    const style = useObserver(() => ({
        color: 'blue',
        cursor: 'pointer',
        fontWeight: (todoStore.filter === name) ? 'bold' : 'normal'
    }))

在这种情况下,当todoStore.filter值发生变化时,style对象将被重新计算,这将导致元素重新渲染,并在选择不同的过滤器时更改字体加粗。

示例代码

示例代码可以在Chapter13/chapter13_2文件夹中找到。

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

使用本地存储 Hook

除了提供全局存储以存储应用程序范围的状态外,MobX 还提供了本地存储以存储本地状态。要创建本地存储,我们可以使用useLocalStore Hook。

现在,我们将在AddTodo组件中实现 Local Store Hook:

  1. 编辑src/AddTodo.js并导入useLocalStore Hook,以及从mobx-react导入useObserver Hook:
import { useLocalStore, useObserver } from 'mobx-react'
  1. 然后,删除以下 State Hook:
    const [ input, setInput ] = useState('')

用本地存储 Hook 替换它:

    const inputStore = useLocalStore(() => ({

在这个本地存储中,我们可以定义状态值、计算值和动作。useLocalStore Hook 将自动将值装饰为可观察的,getter 函数(get前缀)作为计算值,普通函数作为动作。

  1. 我们从input字段的value状态开始:
        value: '',
  1. 然后,我们定义一个计算值,它将告诉我们添加按钮是否应该被disabled
        get disabled () {
            return !this.value
        },
  1. 接下来,我们定义动作。第一个动作从输入事件更新value
        updateFromInput (e) {
            this.value = e.target.value
        },
  1. 然后,我们定义另一个动作来更新value,从一个简单的字符串:
        update (val) {
            this.value = val
        }
    }))
  1. 现在,我们可以调整输入处理函数,并调用updateFromInput动作:
    function handleInput (e) {
        inputStore.updateFromInput(e)
    }
  1. 我们还需要调整handleAdd函数:
    function handleAdd () {
        if (inputStore.value) {
            todoStore.addTodo(inputStore.value)
            inputStore.update('') }}
  1. 最后,我们用useObserver Hook 包装元素,以确保input字段的值在更改时得到更新,并调整disabledvalue属性:
    return useObserver(() => (
        <form onSubmit={e => { e.preventDefault(); handleAdd() }}>
            <input
                type="text"
                placeholder="enter new task..."
                style={{ width: 350, height: 15 }}
                value={inputStore.value}
                onChange={handleInput}
            />
            <input
                type="submit"
                style={{ float: 'right', marginTop: 2 }}
                disabled={inputStore.disabled}
                value="add"
            />
        </form>
    ))
}

现在,我们的AddTodo组件使用一个本地 MobX 存储来处理其输入值,并禁用/启用按钮。如你所见,使用 MobX,可以使用多个存储,用于本地和全局状态。难点在于决定如何分割和分组你的存储,以使其对给定应用程序有意义。

示例代码

示例代码可以在Chapter13/chapter13_3文件夹中找到。

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

迁移 MobX 应用程序

在上一节中,我们学习了如何用 Hooks 替换现有 MobX 应用程序中的 MobX 高阶组件,如injectobserver。现在,我们将学习如何将现有 MobX 应用程序中的本地状态迁移到 Hooks 中。

通过以下三个步骤,可以将现有的 MobX 应用程序迁移到基于 Hook 的解决方案:

  • 使用 State Hook 处理简单的本地状态

  • 使用useLocalState Hook 处理复杂的本地状态

  • 将全局状态保留在单独的 MobX 存储中

我们已经学习了如何在本书的早期章节中使用 State Hook。State Hooks 对于简单的状态,比如复选框的当前状态,是有意义的。

我们已经在本章中学习了如何使用useLocalState Hook。我们可以使用本地状态 Hook 处理复杂的本地状态,比如多个字段相互交互的复杂表单。然后,我们可以用单个本地状态 Hook 替换多个 State 和 Effect Hooks 以及计算值和动作。

最后,全局状态应该存储在单独的 MobX 存储中,例如在本章中定义的TodoStore。在 MobX 中,可以创建多个存储并使用Provider组件传递给组件。然后我们可以为每个存储创建一个单独的自定义 Hook。

MobX 的权衡取舍

总之,让我们总结在 Web 应用程序中使用 MobX 的利弊。首先,让我们从积极方面开始:

  • 它提供了一种简单的处理状态变化的方式

  • 需要更少的样板代码

  • 它提供了灵活性,可以结构化我们的应用程序代码

  • 可以使用多个全局和本地存储

  • 它使App组件更简单(它将状态管理和操作转移到 MobX)

MobX 非常适合处理复杂状态变化和在许多组件中使用的状态的小型和大型项目。

然而,使用 MobX 也有缺点:

  • 状态变化可能发生在任何地方,不仅仅是在单个存储中

  • 它的灵活性意味着可能以不好的方式构建项目,这可能会导致错误或缺陷

  • MobX 需要一个包装组件(Provider)来连接应用程序到存储,如果我们想要获得所有功能(我们可以直接导入和使用 MobX 存储,但这将破坏诸如服务器端渲染之类的功能)

如果状态变化很简单,并且只需要组件内部的本地状态,就不应该使用 MobX。在这种情况下,状态或 Reducer Hook 可能足够了。使用 Reducer 和 State Hooks,我们不需要包装组件来连接我们的应用程序到存储。

灵活性是一件好事,但它也可能导致我们不好地构建项目。然而,MobX 提供了一个名为mobx-state-tree的项目,它允许我们使我们的 MobX 应用程序更有结构并强制执行某种类型的架构。更多信息可以在以下 GitHub 存储库的项目页面中找到:github.com/mobxjs/mobx-state-tree

总结

在本章中,我们首先学习了 MobX 是什么,它由哪些元素组成,以及它们如何一起工作。然后,我们学习了如何在实践中使用 MobX 进行状态管理。我们还学习了如何将 MobX 存储与 React 组件连接起来,使用injectobserver高阶组件。接下来,我们用 Hooks 替换了高阶组件,使我们的代码更加清晰简洁。我们还学习了如何使用 Local Store Hook 处理 MobX 中复杂的本地状态。最后,我们学习了如何将现有的 MobX 应用迁移到 Hooks,并总结了使用 MobX 的权衡。

本章标志着本书的结束。在本书中,我们从动机开始使用 Hooks。我们了解到在 React 应用中有一些常见问题,如果没有 Hooks,很难解决。然后,我们使用 Hooks 创建了我们的第一个组件,并将其与基于类的组件解决方案进行了比较。接下来,我们深入学习了各种 Hooks,从最常见的 State Hook 开始。我们还学习了如何使用 Hooks 解决常见问题,例如条件性 Hooks 和循环中的 Hooks。

在深入学习了 State Hook 之后,我们使用 Hooks 开发了一个小型博客应用。然后,我们学习了 Reducer Hooks、Effect Hooks 和 Context Hooks,以便能够在我们的应用中实现更多功能。接下来,我们学习了如何使用 Hooks 有效地请求资源。此外,我们学习了如何使用React.memo防止不必要的重新渲染,以及如何使用 React Suspense 实现延迟加载。然后,我们在我们的博客应用中实现了路由,并学习了 Hooks 如何使动态路由变得更加容易。

我们还学习了社区提供的各种 Hooks,这些 Hooks 使处理输入字段、各种数据结构、响应式设计和撤销/重做功能变得更加容易。此外,我们学习了 Hooks 的规则,如何创建我们自己的自定义 Hooks,以及 Hooks 之间的交互方式。最后,我们学习了如何有效地从现有的基于类的应用迁移到基于 Hooks 的解决方案。最后,我们学习了如何将 Hooks 与 Redux 和 MobX 一起使用,以及如何将现有的 Redux 和 MobX 应用迁移到 Hooks。

现在我们已经深入了解了 Hooks,我们准备在我们的应用程序中使用它们!我们还学会了如何将现有项目迁移到 Hooks,所以我们现在可以开始做这个。我希望你喜欢学习 React Hooks,并且期待在你的应用程序中实现 Hooks!我相信使用 Hooks 会让编码对你来说更加愉快,就像对我一样。

问题

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

  1. 哪些元素构成 MobX 生命周期?

  2. MobX 提供哪些装饰器?

  3. 我们如何将组件连接到 MobX?

  4. MobX 提供哪些 Hooks?

  5. 我们如何使用 Hooks 访问 MobX 存储?

  6. 我们可以使用 MobX 存储本地状态吗?

  7. 我们应该如何将现有的 MobX 应用程序迁移到 Hooks?

  8. 使用 MobX 的优点是什么?

  9. 使用 MobX 有哪些缺点?

  10. 何时不应该使用 MobX?

进一步阅读

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

第十四章:评估

问题的答案

在这里,我们回答每章末尾提出的所有问题。您可以使用这些问题来回顾您在整本书中学到的知识。

第一章:介绍 React 和 React Hooks

  1. React 的三个基本原则是什么?
  • 声明性的: 我们告诉 React 我们想要什么,而不是告诉 React 如何做事。因此,我们可以轻松设计我们的应用程序,当数据发生变化时,React 将高效地更新和渲染恰当的组件。

  • 基于组件的: React 封装了管理自己状态和视图的组件,然后允许我们组合它们以创建复杂的用户界面。

  • 学一次,随处编写: React 不对您的技术堆栈做出假设,并努力确保您可以尽可能少地重写现有代码来开发。

  1. React 中有哪两种类型的组件?
  • 函数组件: 接受 props 作为参数并返回用户界面的 JavaScript 函数(通常通过 JSX)

  • 类组件: 提供render方法的 JavaScript 类,该方法返回用户界面(通常通过 JSX)

  1. 在 React 中类组件有哪些问题?
  • JavaScript 类对开发人员来说也很难理解:this上下文可能令人困惑,有时我们不得不同时在多个地方编写代码。

  • 它们对机器也很难理解:很难说哪些方法会被调用,因此性能优化实际上并不可行。

    • 它们不是声明性的,因此违反了 React 的基本原则:要使用 React 功能,我们必须编写代码告诉 React 要做什么,而不是如何做。
  1. 在 React 中使用高阶组件的问题是什么?
  • 使用高阶组件会向我们的视图树引入实际上在视图结构方面并不重要的组件。拥有许多高阶组件会导致所谓的包装器地狱
  1. 我们可以使用哪个工具来设置 React 项目,我们需要运行什么命令来使用它?
  • 我们可以使用create-react-app。要创建一个新项目,我们必须运行npx create-react-app <app-name>yarn create react-app <app-name>
  1. 如果我们在类组件中遇到以下错误,我们需要做什么:TypeError: undefined is not an object (evaluating 'this.setState')
  • 我们忘记在类的constructor中重新绑定方法的this上下文。结果,this指向的不是类,而是输入字段的上下文。
  1. 我们如何使用 Hooks 访问和设置 React 状态?
  • 我们使用useState() Hook 如下:const [ name, setName ] = useState('')
  1. 与类组件相比,使用带有 Hooks 的函数组件的优势是什么?
  • 具有 Hooks 的函数组件不会遇到与类相同的问题。它们是声明性的,因此更适合 React 的基本原则。Hooks 还使我们的代码更简洁,更易于理解。
  1. 在更新 React 时,我们是否需要用 Hooks 替换所有类组件?
  • 不,我们不需要替换所有类组件。带有 Hooks 的函数组件可以与现有的类组件并存,并且是 100%向后兼容的。我们可以简单地使用 Hooks 编写新组件,或者以自己的步调升级现有组件。
  1. React 提供的三个基本 Hooks 是什么?
  • useStateuseEffectuseContext Hooks 是 React 提供的基本 Hooks,并在项目中经常使用。但是,React 还提供了一些更高级的 Hooks。

第二章:使用 State Hook

  1. 在开发自己的useState hook 的重新实现时,我们遇到了什么问题?我们是如何解决这些问题的?
  • 一个问题是每次组件被渲染时值的初始化。我们通过使用全局变量来存储值来解决了这个问题。

  • 然后,我们遇到了多个 Hooks 写入同一个全局变量的问题。为了解决这个问题,我们将值存储在数组中,并通过为每个 Hook 分配索引来跟踪当前 Hook。

  1. 为什么在 React 的 Hooks 实现中不可能有条件 Hooks?
  • 条件 Hooks 是不可能的,因为 React 使用 Hook 定义的顺序来跟踪值。如果我们稍后更改 Hooks 的顺序,值将分配给不同的 Hooks。
  1. Hooks 是什么,它们处理什么?
  • Hooks 是处理 React 应用程序中状态和效果的函数
  1. 在使用 Hooks 时,我们需要注意什么?
  • 我们需要确保 Hooks 的顺序始终保持不变,因此我们不能在循环或条件语句中使用 Hooks
  1. Hooks 的替代 API 想法的常见问题是什么?
  • 命名钩子存在名称冲突的问题。即使在库中使用钩子时,每个钩子也必须具有唯一的名称。

  • 钩子工厂需要更多的样板代码,主要是实例化每个钩子两次,一次在组件外部,一次在组件内部。此外,它们使得创建自定义钩子变得更加困难。

  1. 我们如何实现条件钩子?
  • 在简单情况下,我们总是可以定义钩子。否则,我们必须拆分组件,并有条件地渲染一个单独的组件,而不是有条件地渲染钩子。
  1. 我们如何在循环中实现钩子?
  • 在简单情况下,我们可以将数组存储在状态钩子中。否则,我们必须拆分组件并在循环中渲染一个单独的组件。

第三章:使用 React Hooks 编写您的第一个应用程序

  1. 在 React 中,文件结构的最佳实践是什么?
  • 首先从一个简单的结构开始,需要时再进行更深层次的嵌套。在启动项目时,不要花太多时间考虑文件结构。
  1. 在拆分 React 组件时应该使用哪个原则?
  • 单一职责原则,即每个组件应对功能的单个封装部分负责
  1. map函数是做什么的?
  • map函数将给定的函数应用于数组的所有元素,并返回具有结果的新数组
  1. 解构是如何工作的,我们什么时候使用它?
  • 通过解构,我们可以通过在赋值的左侧指定结构和变量名称来从对象中获取属性或从数组中获取元素。我们可以使用解构来获取 React 组件中的某些 props。
  1. 扩展运算符是如何工作的,我们什么时候使用它?
  • 扩展运算符在另一个对象/数组的特定点插入对象的所有属性或数组的所有元素。它可以用于创建新的数组或对象,或者将对象的所有属性作为 props 传递给 React 组件。
  1. 我们如何使用 React Hooks 处理输入字段?
  • 我们为输入字段值创建一个状态钩子,并定义一个设置值的处理函数
  1. 本地状态钩子应该在哪里定义?
  • 本地状态钩子应始终在使用它们的组件中定义
  1. 什么是全局状态?
  • 全局状态是在整个应用程序中跨多个组件使用的状态
  1. 全局状态钩子应该在哪里定义?
  • 全局状态 Hook 应该尽可能高地定义在组件树中。在我们的例子中,我们在 App 组件中定义了它们。

第四章:使用 Reducer 和 Effect Hooks

  1. State Hook 的常见问题是什么?
  • 使用 State Hook 很难进行复杂的状态改变
  1. 什么是动作?
  • 动作是描述状态改变的对象,例如,{ type: 'CHANGE_FILTER', byAuthor: 'Daniel Bugl' }
  1. 什么是 reducer?
  • Reducer 是处理状态改变的函数。它们接受当前状态和一个动作对象,并返回一个新状态。
  1. 何时应该使用 Reducer Hook 而不是 State Hook?
  • 当需要复杂的状态改变时,应该使用 Reducer Hook。通常,这适用于全局状态。

  • 当多个 State Hook 的 setter 函数一起被调用时,这是使用 Reducer Hook 的一个很好的指标。

  1. 为了将 State Hook 转换为 Reducer Hook,需要哪些步骤?
  • 我们首先需要定义动作,然后是 reducer 函数,最后是 Reducer Hook
  1. 我们如何更容易地创建动作?
  • 我们可以定义返回动作对象的函数,称为动作创建者
  1. 何时应该合并 Reducer Hook?
  • 当我们想要避免有两个单独的 dispatch 函数或者同一个动作修改多个 reducer 中的状态时
  1. 在合并 Reducer Hook 时需要注意什么?
  • 我们需要确保每个 reducer 对于未处理的动作返回当前状态
  1. 在类组件中,Effect Hook 的等价物是什么?
  • 在 React 类组件中,我们将使用 componentDidMountcomponentDidUpdate 来处理效果
  1. 使用 Effect Hook 相对于类组件有什么优势?
  • 使用 Effect Hook 时,我们不需要同时定义 componentDidMountcomponentDidUpdate。此外,Effect Hook 更容易理解,我们不需要知道 React 内部的工作原理就能使用它们。

第五章:实现 React 上下文

  1. 上下文避免了哪些问题?
  • 上下文避免了必须通过多个组件层级传递 props 的问题
  1. 上下文由哪两部分组成?
  • React 上下文由提供者和消费者组成
  1. 使用上下文是否需要两部分都定义?
  • 提供者不是必需的,因为当没有定义提供者时,上下文将使用传递给 React.createContext 的默认值
  1. 使用 Hooks 而不是传统上下文消费者的优势是什么?
  • Hooks 不需要使用组件和渲染 props 来进行消费。使用多个上下文和消费者组件会使我们的组件树变得非常深,使我们的应用程序更难调试和维护。Hooks 通过允许我们通过简单调用 Hook 函数来消费上下文来避免这个问题。
  1. 上下文的替代方案是什么,什么时候应该使用它?
  • 上下文使得重用组件变得更加困难。只有在我们需要在不同嵌套级别的多个组件中访问数据时,才应该使用上下文。否则,我们可以通过传递 props 或传递渲染的组件来使用控制反转技术。
  1. 我们如何实现动态更改上下文?
  • 我们需要使用 State Hook 来为上下文提供值
  1. 什么时候使用上下文来管理状态是有意义的?
  • 通常情况下,使用上下文来管理全局状态是有意义的,这些状态在不同嵌套级别的多个组件中使用。

第六章:实现请求和 React Suspense

  1. 如何从一个简单的 JSON 文件中轻松创建一个完整的 REST API?
  • 我们可以使用json-server工具从 JSON 文件创建一个完整的 REST API,用于开发和测试
  1. 在开发过程中使用代理访问后端服务器的优势是什么?
  • 在使用代理时,在开发过程中我们不需要处理跨站点限制
  1. 我们可以使用哪些组合的 Hooks 来实现请求?
  • 我们可以使用 Effect 和 State 或 Reducer Hook 来实现请求
  1. 我们可以使用哪些库来实现请求?
  • 我们还可以使用axiosreact-request-hook库来实现请求
  1. 我们如何使用react-request-hook处理加载状态?
  • 我们可以使用从useResource Hook 返回的result.isLoading标志,并有条件地显示加载消息
  1. 我们如何使用react-request-hook处理错误?
  • 我们可以使用从useResource Hook 返回的result.error对象并分派错误操作
  1. 如何防止组件不必要的重新渲染?
  • 使用React.memo,我们可以防止不必要的重新渲染,类似于shouldComponentUpdate
  1. 我们如何减少应用程序的捆绑大小?
  • 我们可以使用React.Suspense来延迟加载某些组件,这意味着只有在需要时才会从服务器请求它们。

第七章:使用 Hooks 进行路由

  1. 为什么我们需要定义单独的页面?
  • 大多数大型应用程序由多个页面组成。例如,每篇博客文章都有一个单独的页面
  1. 我们如何使用 Navi 库定义路由?
  • 我们使用mount函数并传递一个将路径映射到route函数的对象
  1. 我们如何使用 URL 参数定义路由?
  • 我们可以使用:parameter语法在路径中指定 URL 参数
  1. 如何使用 Navi 定义静态链接?
  • 可以使用react-navi中的Link组件来定义静态链接
  1. 我们如何实现动态导航?
  • 可以使用useNavigation Hook 并调用navigation.navigate()来实现动态导航
  1. 用于访问当前路由信息的 Hook 是什么?
  • useCurrentRoute Hook 为我们提供了关于当前路由的所有信息
  1. 用于访问当前加载路由的 Hook 是什么?
  • useLoadingRoute Hook 为我们提供了关于当前正在加载的路由的所有信息

第八章:使用社区 Hooks

  1. 我们可以使用哪个 Hook 来简化输入字段处理?
  • 我们可以使用react-hookedup库中的useInput Hook
  1. 如何使用 Effect Hooks 实现componentDidMountcomponentWillUnmount生命周期?
  • componentDidMount可以通过使用 Effect Hook 并将空数组作为第二个参数传递来实现。例如,useEffect(() => console.log('did mount'), [])

  • componentWillUnmount可以通过从 Effect Hook 返回一个函数来实现,其中空数组作为第二个参数传递,例如,useEffect(() => { return () => console.log('will unmount') }, [])

  1. 我们如何使用 Hooks 来获得this.setState()的行为?
  • this.setState()将现有状态对象与给定状态对象合并。我们可以使用useMergeState Hook 来获得相同的行为,而不是简单的 State Hook。
  1. 为什么我们应该使用定时器 Hooks 而不是直接调用setTimeoutsetInterval
  • 在定义简单的超时或间隔时,当组件重新渲染时它们将被重置。为了防止这种重置发生,我们必须使用react-hookedup中的useTimeoutuseInterval Hook。
  1. 我们可以使用哪些 Hooks 来简化处理常见数据结构?
  • 我们可以使用react-hookedup中的useBooleanuseArrayuseCounter Hooks
  1. 何时应该使用 Hooks 进行响应式设计,而不是简单地使用 CSS 媒体查询?
  • 当在画布或 WebGL 中渲染元素时,或者当我们动态地想要根据窗口大小决定是否加载组件时,我们应该使用 Hooks 进行响应式设计
  1. 我们可以使用哪个 Hook 来实现撤销/重做功能?
  • 我们可以使用use-undo库中的useUndo Hook 来在我们的应用程序中实现简单的撤销/重做功能
  1. 什么是防抖动?为什么我们需要这样做?
  • 防抖动意味着函数只会在一定时间后被调用,而不是每次事件触发时都被调用。使用防抖动,我们可以在文本字段中输入的值仅在每秒后存储在撤销历史中,而不是在每次输入字符后。
  1. 我们可以使用哪个 Hook 来进行防抖动?
  • 我们可以使用use-debounce库中的useDebounceuseDebouncedCallback Hook

第九章:Hook 的规则

  1. Hook 可以在哪里调用?
  • Hook 只能在 React 函数组件或自定义 Hook 的开头调用
  1. 我们可以在 React 类组件中使用 Hook 吗?
  • 不,不可能在 React 类组件中使用 Hook
  1. 关于 Hook 的顺序,我们需要注意什么?
  • Hook 的顺序不应该改变,因为它用于跟踪各种 Hook 的值
  1. Hook 可以在条件语句、循环或嵌套函数中调用吗?
  • 不,Hook 不能在条件语句、循环或嵌套函数中调用,因为那样会改变 Hook 的顺序
  1. Hook 的命名约定是什么?
  • Hook 函数名称应始终以use前缀开头,然后是CamelCase中的名称。例如:useSomeHookName
  1. 我们如何自动强制执行 Hook 的规则?
  • 我们可以使用eslinteslint-plugin-react-hooks来强制执行 Hook 的规则
  1. 详尽的依赖规则是什么?
  • 详尽的依赖规则确保在 Effect Hook 中使用的所有变量都通过第二个参数列为依赖项
  1. 我们如何自动修复 linter 警告?
  • 我们可以运行npm run lint -- --fix命令来自动修复 linter 警告。例如,运行此命令将自动将 Effect Hook 中使用的所有变量输入为依赖项。

第十章:构建自己的 Hooks

  1. 我们如何从现有代码中提取自定义 Hook?
  • 我们可以简单地将我们的代码放入一个单独的函数中。在自定义 Hook 函数中可以使用其他 Hook 函数,但我们需要确保不违反 Hook 的规则。
  1. 创建 API Hooks 的优势是什么?
  • 当为各种 API 调用定义单独的函数时,如果 API 以后发生更改,我们可以很容易地调整它们,因为我们把所有与 API 相关的代码放在一个地方
  1. 我们何时应该将功能提取到自定义 Hook 中?
  • 当某个功能在多个地方使用或以后可能被重复使用时,我们应该创建一个自定义 Hook
  1. 我们如何使用自定义 Hooks?
  • 我们可以像调用官方 React Hooks 或来自库的 Hooks 一样简单地调用自定义 Hooks
  1. 何时应该创建本地 Hooks?
  • 当我们想要将某个功能封装在单独的函数中,但它只会在单个组件中使用时,可以使用本地 Hooks
  1. Hooks 之间有哪些交互?
  • 我们可以在 Hook 函数中使用其他 Hooks,并且可以从其他 Hooks 传递值到 Hooks
  1. 我们可以使用哪个库来测试 Hooks?
  • 我们可以使用jest测试运行器与 React Hooks 测试库(@testing-library/react-hooks)和react-test-renderer来测试 Hooks
  1. 我们如何测试 Hook 动作?
  • 可以使用act函数来测试 Hook 动作。例如,act(() => result.current.increment())
  1. 我们如何测试上下文?
  • 通过编写一个上下文包装函数来测试上下文,该函数返回提供者。然后可以将包装函数传递给renderHook函数。例如,const { result } = renderHook(() => useTheme(), { wrapper: ThemeContextWrapper })
  1. 我们如何测试异步代码?
  • 我们可以使用与从renderHook返回的waitForNextUpdate函数结合使用 async/await 构造来等待异步代码完成运行

第十一章:从 React 类组件迁移

  1. React 类组件是如何定义的?
  • React 类组件是通过使用class ComponentName extends React.Component {来定义的
  1. 在使用类组件的constructor时,我们需要调用什么?为什么?
  • 我们首先需要调用super(props)来确保 props 被传递给React.Component
  1. 我们如何在类组件中设置初始状态?
  • 我们可以通过在constructor中定义this.state对象来在类组件中设置初始状态
  1. 我们如何改变类组件的状态?
  • 在类组件中,我们使用this.setState()来改变状态
  1. 为什么我们需要重新绑定类组件方法的this上下文?
  • 当将方法传递给元素作为事件处理程序时,this上下文会更改为触发事件的元素。我们需要重新绑定this上下文到类以防止这种情况发生。
  1. 我们如何重新绑定this上下文?
  • 我们需要在构造函数中对方法使用.bind(this)。例如,this.handleInput = this.handleInput.bind(this)
  1. 如何在类组件中使用 React 上下文?
  • 我们可以设置contextType,然后访问this.context。例如,static contextType = StateContext

  • 如果我们想使用多个上下文,我们可以使用上下文消费者。例如,<StateContext.Consumer>{value => <div>State is: {value}</div>}</StateContext.Consumer>

  1. 在迁移到 Hooks 时,我们可以用什么替换状态管理?
  • 我们可以用 State Hook 替换this.statethis.setState
  1. 使用 Hooks 与类组件相比有哪些权衡?
  • 使用 Hooks 的函数组件更简单(不需要处理构造函数、this或多次解构相同的值,处理上下文、props 和状态时没有魔法,不需要同时定义componentDidMountcomponentDidUpdate)。函数组件还鼓励创建小而简单的组件,更容易重构和测试,需要更少的代码,对初学者更容易理解,更具有声明性。

  • 然而,当遵循特定约定并使用最新的 JavaScript 特性来避免this重新绑定时,类组件可能是可以的。此外,由于具有现有知识,类组件可能更容易为团队理解。

  1. 何时以及如何将现有项目迁移到 Hooks?
  • 在适当的时候逐步用基于 Hook 的函数组件替换旧的类组件。例如,当您已经在重构一个组件时。

第十二章:Redux 和 Hooks

  1. Redux 应该用于哪种状态?
  • Redux 应该用于全局状态,即在我们的应用程序中多个组件中使用的状态。
  1. Redux 由哪些元素组成?
  • Redux 由store(描述应用程序完整状态的对象)、actions(描述状态修改的对象)、action creators(创建操作对象的函数)、reducers(接受当前状态和操作对象并返回新状态的函数)和connectors(将现有组件连接到 Redux 的高阶组件)组成
  1. Redux 的三个原则是什么?
  • 单一数据源(数据应始终具有单一来源)

  • 只读状态(不可能直接修改状态,只能通过分派操作来修改)

  • 状态更改通过纯函数处理(给定相同的状态和操作,reducer 将始终返回相同的新状态)

  1. 为什么要定义操作类型?
  • 动作类型避免在定义或比较动作的type属性时出现拼写错误。
  1. 我们如何将组件连接到 Redux?
  • 我们可以使用connect高阶组件,也可以使用 Dispatch 和 Selector Hooks。
  1. 我们可以使用哪些 Hooks 与 Redux 一起使用?
  • 使用useDispatch来获取分发函数,使用useSelector来获取状态的某个部分,使用useStore来获取 Redux 存储(用于特殊用例,比如替换 reducers)。
  1. 为什么我们应该创建可重用的选择器?
  • 可重用的选择器可以在多个组件中使用。此外,它们会记忆结果,并且只在状态改变时重新计算。
  1. 我们如何迁移 Redux 应用程序?
  • 我们应该首先用 State Hooks 替换简单的本地状态,比如输入字段的值。然后用 Reducer Hooks 替换复杂的本地状态。我们将全局状态(在多个组件中使用)保留在 Redux 存储中。最后,我们使用 Selector 和 Dispatch Hooks 来代替connect高阶组件。
  1. Redux 的权衡是什么?
  • 使用 Redux 的优点是:它提供了一定的项目结构,使我们可以轻松地扩展和修改代码,代码中的错误可能性较少,它的性能比仅使用 React 上下文来管理状态要好,并且通过将状态管理和动作创建者转移到 Redux,使我们的App组件变得更简单。

  • 使用 Redux 的缺点是:它需要大量样板代码,项目结构变得更加复杂,并且需要一个包装组件(Provider)来连接应用程序到存储。

  1. 我们何时应该使用 Redux?
  • 我们应该只在需要复杂状态更改的应用程序中使用 Redux。对于简单项目,Reducer Hooks 甚至只使用 State Hooks 可能就足够了。

第十三章:MobX 和 Hooks

  1. 哪些元素构成 MobX 的生命周期?
  • 事件调用动作,从而修改状态。状态是可观察的,不应包含冗余或可推导的数据。计算值是通过纯函数从状态派生出来的。反应类似于计算值,但它们也可以产生副作用,比如在 React 中更新用户界面。
  1. MobX 提供哪些装饰器?
  • MobX 为各种元素提供装饰器:observerobservablecomputedaction
  1. 我们如何将组件连接到 MobX?
  • 我们可以通过使用Provider组件将我们的应用连接到 MobX 存储,然后通过inject高阶组件连接组件。如果我们希望组件在状态更改时自动重新渲染,还需要使用observer修饰函数来包装它。
  1. MobX 提供了哪些 Hooks?
  • 我们可以使用useObserver Hook 来定义组件的部分,在状态更改时应该重新计算。
  1. 如何使用 Hooks 访问 MobX 存储?
  • MobX 提供了一个上下文,可以用来创建访问 MobX 存储的自定义 Hooks。我们可以使用普通的上下文 Hook 来访问mobx-react中的MobXProviderContext
  1. 我们可以使用 MobX 存储本地状态吗?
  • 是的,使用 MobX,我们可以创建任意数量的存储。MobX 甚至提供了一个useLocalStore Hook 来创建本地存储。
  1. 我们应该如何将现有的 MobX 应用迁移到 Hooks?
  • 我们可以逐步升级 MobX 应用的某些部分。我们可以使用自定义 Hook 来访问上下文的一部分,而不是inject高阶组件。我们可以使用useObserver Hook 来替代observer高阶组件。

  • 我们应该首先使用 State Hook 来处理简单的本地状态,然后使用useLocalState Hook 来处理复杂的本地状态,最后将全局状态保留在单独的 MobX 存储中。

  1. 使用 MobX 的优势是什么?
  • 它提供了处理状态更改的简单方式,需要更少的样板代码,在应用代码结构方面提供了更多的灵活性,允许使用多个全局和本地存储,并通过将状态管理和操作交给 MobX 使App组件变得更简单。
  1. 使用 MobX 的缺点是什么?
  • 它允许状态更改发生在任何地方,而不仅仅是在单个存储中,这可能会使我们的应用更加不可预测。更多的灵活性也意味着可能以不好的方式构建项目并导致错误或缺陷。此外,如果我们想要获得所有功能,MobX 需要一个包装组件将应用连接到存储(我们可以直接导入和使用 MobX 存储,但这将破坏诸如服务器端渲染之类的功能)。
  1. 什么时候不应该使用 MobX?
  • 如果状态更改简单且仅使用组件内的本地状态,则不应使用 MobX。在这种情况下,状态和 Reducer Hooks 可能足够了。
posted @ 2024-05-16 14:50  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报