第 2 章 使用 State Hook

第 2 章 使用 State Hook

Flying
2020-12-23 / 0 评论 / 162 阅读 / 正在检测是否收录...

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

本章将介绍以下主题:

  • useState Hook 重新实现为一个简单的函数,它访问全局状态
  • 将我们的重新实现与真正的 React Hook 进行比较并了解差异
  • 了解可能的替代 Hook API 及其权衡
  • 解决由 Hook 限制导致的常见问题
  • 使用条件 Hook 解决问题

技术要求

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

本章的代码可以在 GitHub 存储库中找到:https://github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter02
观看以下视频,了解代码的实际应用:

http://bit.ly/2Mm9yoC

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

现在,让我们从本章开始。

重新实现 useState 函数

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

请注意,这种重新实现并不完全是 React Hook 内部的工作方式。实际实现是相似的,因此具有类似的约束。但是,真正的实现比我们在这里实现的要复杂得多。

我们现在将开始重新实现 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 Hook,这将在内部处理。
  1. 现在,我们定义自己的 useState 函数。正如我们已经知道的,useState 函数将 initialState 作为参数:
function useState (initialState) {
  1. 然后,我们定义一个值,我们将在其中存储我们的状态。首先,此值将设置为 initialState,作为参数传递给函数:
let value = initialState
  1. 接下来,我们定义 setState 函数,我们将 value 设置为不同的值,并强制重新渲染我们的 MyName 组件:
function setState (nextValue) { 
  value = nextValue
  ReactDOM.render(<MyName />, document.getElementByid('root'))
}
  1. 最后,我们将 valuesetState 函数作为数组返回:
  return [ value, setState ]
}

使用数组而不是对象的原因是我们通常想要重命名 valuesetState 变量。使用数组可以通过解构轻松重命名变量:

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

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

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

简单 Hook 实现的问题

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

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

使用全局变量

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

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

  1. 首先,我们在 useState 函数定义上方添加第 1 行:
let value
function useState (initialState) {
  1. 然后,我们将函数中的第 1 行替换为以下代码:
if (typeof value === 'undefined') value = initialState

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

定义多个 Hook

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

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

向我们的组件添加多个 Hook

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

  1. 我们首先在函数的开头创建一个新的 Hook(第 2 行),在当前 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 函数的第 1 行,我们现在初始化值数组的 currentHook 索引处的值:
if (typeof values[currentHook] === 'undefined') 
    values[currentHook] = initialState
  1. 我们还需要更新 setter 函数,以便只更新相应的状态值。在这里,我们需要将 currentHook 值存储在一个单独的 hookIndex 变量中,因为 currentHook 值稍后会发生变化。这可确保在 useState 函数的闭包中创建当前 Hook 变量的副本。否则,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 增加 1。这意味着从函数返回后,currentHook 将增加。

如果我们想先递增一个值,然后使用它,我们可以使用 arr[++indexToBeIncremented] 语句,该语法首先递增,然后将结果传递给数组。
  1. 当我们开始渲染组件时,我们仍然需要重置 currentHook 计数器。在组件定义之后添加如下第 2 行代码:
function Name () {
  currentHook = 0

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

app-default.jpg
我们的自定义 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. 如果名字被禁用,我们不想显示它。编辑以下现有行,添加对变量 的检查:
<h1>My name is: {enableFirstName ? name : ''} {lastName}</h1>
  1. 我们可以将 Hook 定义放入 if 条件或三元表达式中,就像我们在下面的代码片段中一样吗?
const [ name, setName ] = enableFirstName
  ? useState(''): [ '', () => {} ]
  1. 最新版本的 react-scripts 在定义条件 Hook 时实际上会抛出错误,因此我们需要通过运行以下命令来降级此示例的库:
npm install --save react-scripts@^2.1.8

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

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

app-uncheck.jpg
选中复选框之前的应用程序状态

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

  • 复选框被选中了
  • 名字输入字段已启用
  • 姓氏字段的值现在是名字字段的值

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

app-uncheck.jpg
选中复选框之后的应用程序状态

我们可以看到姓氏状态现在位于名字字段中。这些值已被交换,因为 Hook 的顺序很重要。正如我们从实现中知道的那样,我们使用 currentHook 索引来了解每个 Hook 的状态存储在哪里。但是,当我们在两个现有的 Hook 之间插入一个额外的 Hook 时,顺序就会搞砸。

在选中复选框之前,values 数组如下所示:

  • [false, '']
  • Hook 顺序:enableFirstNamelastName

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

  • [false, 'Hook']
  • Hook 顺序:enableFirstNamelastName

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

  • [true, 'Hook', '']
  • Hook 顺序:enableFirstNamenamelastName

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

示例代码

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

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

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

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

通过重新实现 useState Hook,我们学到了几件事:

  • Hook 只是访问 React 功能的函数
  • Hook 处理在重新渲染中持续存在的副作用
  • Hook 定义的顺序很重要

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

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

  • React Hook 需要在 React 函数组件中调用
  • React Hook 不能有条件地定义,也不能在循环中定义

我们现在来看看允许条件 Hook 的替代 Hook APIs。

替代 Hook APIs

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

命名 Hook

我们可以给每个 Hook 起一个名字,然后将 Hook 存储在对象而不是数组中。然而,这不会是一个好的 API,我们还必须始终考虑为Hook 提供唯一的名称:

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

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

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

Hook 工厂

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

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('')
  // ...
}

但是,这意味着我们需要实例化每个 Hook 两次:一次在我们的组件外部,一次在函数组件内部。这为错误创造了更多的空间。例如,如果我们创建两个 Hook 并复制并粘贴样板代码,那么我们可能会在工厂函数中产生一个错误的Hook名称,或者可能会在组件中使用 Hook 时出错。

这种方法也使得创建自定义 Hook 变得更加困难,这迫使我们编写包装器函数。此外,调试这些包装的函数比调试一个简单函数更难。

其他替代方案

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

最后,React 团队决定最简单的 API 是通过计算 Hook 的调用顺序来跟踪它们。这种方法有其自身的缺点,例如无法有条件地或在循环中调用 Hook。但是,这种方法使我们很容易创建自定义 Hook,并且易于使用和调试。我们也不需要担心命名 Hook、名称冲突或编写包装器函数。Hook 的最终方法让我们像使用任何其他函数一样使用 Hook!

解决 Hook 的常见问题

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

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

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

  • 求解条件 Hook
  • 解决循环中的 Hook

求解条件 Hook

那么,我们如何实现条件 Hook呢?我们总是可以定义 Hook 并在需要时使用它,而不是让 Hook 具有条件。如果这不是一个选择,我们需要拆分我们的组件,这通常是更好的选择!

始终定义 Hook

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

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

对于简单情况,始终定义 Hook 通常是一个很好的解决方案。

拆分组件

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

我们不能执行以下操作,因为使用 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>
}

无论如何,对非登录和登录状态使用两个单独的组件是有意义的,因为我们希望坚持每个组件具有一个功能的原则。因此,如果我们坚持最佳实践,通常情况下,不能使用条件 Hook 并不是一个很大的限制。

解决循环中的 Hook

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

使用数组

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

function OnlineUsers({ users }) {
  const [userInfos, setUserInfos] = useState([])
  // 
  // ... 获取并保持 userInfo 是最新的 ... 
  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)
  // ... 获取并保持 userInfo 是最新的 ... 
  return <div>{info}</div>
}

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

使用条件 Hook 解决问题

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

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

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

将其替换为普通的 Hook,如下所示:

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

现在,我们的示例工作正常!在更复杂的情况下,始终定义 Hook 可能不可行。在这种情况下,我们需要创建一个新组件,在那里定义 Hook,然后有条件地渲染该组件。

示例代码

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

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

总结

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

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

问题

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

  1. 我们在开发自己的 useState Hook 重新实现时遇到了什么问题?我们如何解决这些问题?
  2. 为什么条件 Hook 在 React 的 Hook 实现中是不可能的?
  3. 什么是 Hook,它们处理什么?
  4. 使用 Hook 时我们需要注意什么?
  5. Hook 的替代 API 想法的常见问题是什么?
  6. 我们如何实现条件 Hook?
  7. 我们如何在循环中实现 Hook?

延伸阅读

如果您有兴趣了解有关我们在本章中学到的概念的更多信息,请查看以下阅读材料:

1

评论 (0)

取消