八、使用社区挂钩

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

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

本章将介绍以下主题:

  • 使用输入挂钩简化输入处理
  • 用挂钩实现 React 生命周期
  • 学习各种有用的挂钩(usePrevious、定时器、在线、聚焦、悬停和数据操作挂钩)
  • 用挂钩实现响应性设计
  • 使用挂钩实现撤消/重做功能和去抖动
  • 学习在哪里找到其他挂钩

技术要求

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

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

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

http://bit.ly/2Mm9yoC

Please note that it is highly recommended that you write the code on your own. Do not simply run the code examples that have been provided. It is important that you write the code yourself in order to be able to learn and understand properly. However, if you run into any issues, you can always refer to the code example.

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

探索输入处理挂钩

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

useInput挂钩通过提供处理input字段的value变量的单个挂钩,大大简化了这个用例。其工作原理如下:

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

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

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

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

此外,还有一个清除input字段的功能。这个clear函数也是从挂钩返回的:

    const { clear } = useInput('')

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

此外,挂钩提供了两种绑定input字段的方法:

  • bindToInput:使用e.target.value作为onChange函数的value参数,将valueonChange道具绑定到input字段。这在处理 HTMLinput字段时非常有用。
  • bind:仅使用e作为onChange函数的值,将valueonChange道具绑定到input字段。这对于直接将值传递给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}道具,因为我们这里不处理输入事件,并且值直接传递给变更处理程序(不是通过e.target.value)。

现在我们已经了解了输入挂钩,我们可以继续在我们的博客应用中实现它。

在我们的博客应用中实现输入挂钩

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

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

> npm install --save react-hookedup

我们现在将在以下组件中实现输入挂钩:

  • Login组件
  • Register组件
  • CreatePost组件

让我们开始实现输入挂钩。

登录组件

我们在Login组件中有两个input字段:用户名和密码字段。我们现在将用输入挂钩替换状态挂钩。

现在让我们开始在Login组件中实现输入挂钩:

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

将其替换为输入挂钩,如下所示:

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

Since we are using two Input Hooks, in order to avoid name collisions, we are using the rename syntax ({ from: to }) in object destructuring to rename the value key to username, and bindToInput key to 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 组件

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} />

createpost 功能也将以与输入挂钩相同的方式工作。

示例代码

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

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

挂钩和 React 生命周期

正如我们在前几章中所了解的,我们可以使用useEffect挂钩对 React 的大多数生命周期方法进行建模。但是,如果您喜欢直接处理 React 生命周期,而不是使用 Effect 挂钩,那么有一个名为react-hookedup的库,它提供了各种挂钩,包括各种 React 生命周期的挂钩。此外,该库还提供了一个合并状态挂钩,其工作原理与 React 类组件中的this.setState()类似。

useOnMount 挂钩

useOnMount挂钩与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 到控制台。当组件由于(例如)道具更改而重新渲染时,将不会再次调用它。

或者,我们可以使用带有空数组的useEffect挂钩作为第二个参数,其效果相同:

import React, { useEffect } from 'react'

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

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

如我们所见,使用带有空数组的效果挂钩作为第二个参数会导致与useOnMount挂钩或componentDidMount生命周期方法相同的行为。

useOnUnmount 挂钩

useOnUnmount挂钩与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>
}

前面的代码将在卸载组件时(在从 DOM 中移除 React 组件之前)将卸载输出到控制台。

如果您还记得第 4 章中提到的使用简化器和效果挂钩,我们可以从useEffect挂钩返回一个清理功能,组件卸载时会调用该功能。这意味着我们也可以使用useEffect实现useOnMount挂钩,如下所示:

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 挂钩返回的 cleanup 函数,使用一个空数组作为第二个参数,与useOnUnmount挂钩或componentWillUnmount生命周期方法具有相同的效果。

useLifecycleHooks 挂钩

useLifecycleHooks挂钩将前两个挂钩合并为一个。我们可以将useOnMountuseOnUnmount挂钩组合如下:

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>
}

或者,我们可以分别使用两个挂钩:

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挂钩,如下所示:

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,我们可以将整个效果放在一个函数中,然后简单地返回一个函数进行清理。当我们在接下来的章节中学习如何制作自己的挂钩时,这种模式尤其有用。

效应使我们对 React 组件有不同的想法。我们根本不必考虑组件的生命周期。相反,我们考虑的是效果、依赖关系和效果的清理。

useMergeState 挂钩

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

合并状态挂钩返回以下对象:

  • state:当前状态
  • setState:将当前状态与给定状态对象合并的函数

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

  1. 首先,我们导入useState挂钩:
import React, { useState } from 'react'
  1. 然后,我们使用包含loaded值和counter值的对象定义我们的应用组件和状态挂钩:
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挂钩来完全避免这个问题,并获得与this.setState()类组件相同的行为:

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

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

正如我们所看到的,通过使用useMergeState挂钩,我们可以重现与this.setState()类组件相同的行为。因此,我们不再需要使用扩展语法。然而,通常情况下,最好只使用多个状态挂钩或减速机挂钩。

示例代码

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

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

各种有用的挂钩

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

这些挂钩如下:

  • usePrevious挂钩,获取挂钩或道具的先前值
  • 计时器挂钩,用于实现间隔和超时
  • useOnline挂钩,用于检查客户端是否有活动的 internet 连接
  • 用于处理布尔、数组和计数器的各种数据操作挂钩
  • 用于处理焦点和悬停事件的挂钩

上钩

usePrevious挂钩是一个简单的挂钩,可以让我们获得道具或挂钩值的先前值。它将始终存储并返回任何给定变量的先前值,其工作方式如下:

  1. 首先,我们进口useStateusePrevious挂钩:
import React, { useState } from 'react'
import { usePrevious } from 'react-hookedup'
  1. 然后,我们定义我们的App组件,以及一个存储当前count状态的挂钩:
export default function UsePrevious () {
    const [ count, setCount ] = useState(0)
  1. 现在,我们定义usePrevious挂钩,将count值从状态挂钩传递给它:
    const prevCount = usePrevious(count)

The usePrevious Hook works with any variable, including component props and values from other Hooks.

  1. 接下来,我们定义一个 handler 函数,它将count增加1
    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,现在为 0,因为上一个挂钩的默认值为null。单击按钮一次时,将显示以下内容:计数为 0,现在为 1。。

计时器挂钩

react-hookedup库还提供了处理计时器的挂钩。如果我们只是在我们的组件中使用setTimeoutsetInterval创建一个计时器,它将在每次重新呈现组件时再次实例化。这不仅会导致错误和不可预测性,而且如果没有正确释放旧计时器,还会导致内存泄漏。使用计时器挂钩,我们可以完全避免这些问题,并且可以轻松地使用间隔和超时。

库提供了以下计时器挂钩:

  • useInterval挂钩,用于定义 React 组件中的setInterval计时器(多次触发的计时器)
  • useTimeout挂钩,用于定义setTimeout计时器(在一定时间后仅触发一次的计时器)

间隔钩

useInterval挂钩可以像setInterval一样使用。现在,我们将实现一个小型计数器,用于计算安装组件后的秒数:

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

或者,我们可以将效果挂钩与setInterval结合使用,而不是useInterval挂钩,如下所示:

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挂钩使我们的代码更加简洁易读。

使用超时挂钩

useTimeout挂钩可以像setTimeout一样使用。我们现在要实现一个在10秒后触发的组件:

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

或者,我们可以将效果挂钩与setTimeout结合使用,而不是useTimeout挂钩,如下所示:

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挂钩使我们的代码更加简洁易读。

在线状态挂钩

在某些 web 应用中,实现脱机模式是有意义的;例如,如果我们希望能够在本地编辑和保存帖子的草稿,并在再次联机时将其同步到服务器。为了能够实现这个用例,我们可以使用useOnlineStatus挂钩。

在线状态挂钩返回一个带有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>
}

上一个组件将显示您处于联机状态!,当 internet 连接可用或您处于脱机状态时!,否则

然后,我们可以使用前一个挂钩和一个效果挂钩,以便在我们再次联机时将数据同步到服务器:

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>
}

现在,我们有一个效果挂钩,每当online的值改变时就会触发。然后检查之前的online值是否为false,当前值是否为true。如果是这样的话,那就意味着我们离线了,现在又在线了,所以我们需要将更新的数据同步到服务器上。

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

数据操作挂钩

react-hookedup库提供了各种用于处理数据的实用程序挂钩。这些挂钩简化了对公共数据结构的处理,并提供了对状态挂钩的抽象。

提供了以下数据操作挂钩:

  • useBoolean挂钩:处理布尔值的切换
  • useArray挂钩:处理数组
  • useCounter挂钩:对付柜台

布尔挂钩

useBoolean挂钩用于处理布尔值的切换(true/false),并提供将值设置为true/false的函数和切换值的toggle函数。

挂钩返回具有以下内容的对象:

  • value:布尔值的当前值
  • toggle:切换当前值的功能(如果当前为false,则设置true;如果当前为true,则设置false
  • setTrue:将当前值设置为true
  • setFalse:将当前值设置为false

布尔挂钩的工作原理如下:

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

按钮最初将在文本关闭的情况下呈现。单击按钮时,它将在屏幕上显示文本。再次单击时,它将再次关闭。

使用数组挂钩

useArray挂钩用于轻松处理数组,无需使用 rest/spread 语法。

数组挂钩返回具有以下内容的对象:

  • value:当前数组
  • setValue:设置一个新数组作为值
  • add:将给定元素添加到数组中
  • clear:从数组中删除所有元素
  • removeIndex:根据元素的索引从数组中移除元素
  • removeById:通过id从数组中移除元素(假设数组中的元素是具有id键的对象)

其工作原理如下:

  1. 首先,我们从react-hookedup导入useArray挂钩:
import React from 'react'
import { useArray } from 'react-hookedup'
  1. 然后定义组件和数组挂钩,默认值为['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挂钩使处理数组变得更加简单。

反钩

useCounter挂钩可用于定义各种计数器。我们可以定义下限/上限,指定计数器是否应循环,并指定增加/减少计数器的步长。此外,计数器挂钩提供增加/减少计数器的功能。

它接受以下配置选项:

  • upperLimit:定义我们计数器的上限(最大值)
  • lowerLimit:定义我们计数器的下限(最小值)
  • loop:指定计数器是否应循环(例如,当达到最大值时,我们返回最小值)
  • step:设置增加和减少功能的默认步数

它返回以下对象:

  • value:我们计数器的当前值。
  • setValue:设置我们计数器的当前值。
  • increase:将该值增加给定的步长量。如果未指定金额,则使用默认步骤金额。
  • decrease:将该值减少给定的步长量。如果未指定金额,则使用默认步骤金额。

反钩可按如下方式使用:

  1. 首先,我们从react-hookedup导入useCounter挂钩:
import React from 'react'
import { useCounter } from 'react-hookedup'
  1. 然后,我们定义组件和挂钩,将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>
    )
}

正如我们所看到的,计数器挂钩使计数器的实现更加简单。

焦点和悬停挂钩

有时,我们想检查用户是否悬停在某个元素上或关注某个input字段。为此,我们可以使用react-hookedup库提供的焦点和悬停挂钩。

库为这些功能提供了两个挂钩:

  • useFocus挂钩:处理焦点事件(例如,所选input字段)
  • useHover挂钩:处理悬停事件(例如,将鼠标指针悬停在某个区域上时)

聚焦挂钩

为了知道某个元素当前是否被聚焦,我们可以使用useFocus挂钩,如下所示:

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

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

悬停钩

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

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

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

示例代码

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

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

带挂钩的响应性设计

在 web 应用中,具有响应性的设计通常很重要。响应性设计可使您的 web 应用在各种设备和窗口/屏幕大小上呈现良好。我们的博客应用可以在桌面、手机、平板电脑上观看,甚至可以在电视等超大屏幕上观看。

通常,简单地使用 CSS 媒体查询进行响应性设计是最有意义的。然而,有时这是不可能的,例如,当我们在画布或Web 图形库WebGL中渲染元素时。有时,我们还希望使用窗口大小来决定是否加载组件,而不是简单地渲染组件,然后稍后通过 CSS 将其隐藏。

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

  • innerWidth:等于window.innerWidth
  • innerHeight:等于window.innerHeight
  • outerWidth:等于window.outerWidth
  • outerHeight:等于window.outerHeight

要显示outerWidth/outerHeightinnerWidth/innerHeight之间的差异,请看下图:

Visualization of the window width/height properties

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

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

响应隐藏组件

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

让我们开始实现窗口大小挂钩:

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

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

Hiding the ChangeTheme and UserBar components on smaller screen sizes

使用窗口大小挂钩,我们可以避免在较小的屏幕大小上渲染元素。

示例代码

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

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

用挂钩撤消/重做

在某些应用中,我们希望实现撤销/重做功能,这意味着我们可以在应用的状态下来回移动。例如,如果我们的博客应用中有一个文本编辑器,我们希望提供一个撤销/重做更改的功能。如果您了解了 Redux,您可能已经熟悉这种功能。由于 React 现在提供了一个简化器挂钩,因此我们可以仅使用 React 重新实现相同的功能。use-undo库正好提供了这一功能。

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

state对象如下所示:

  • present:当前状态
  • past:过去状态的数组(当我们撤销时,我们转到这里)
  • future:未来状态数组(撤销后可以重做此处)

functions对象返回各种与撤销挂钩交互的函数:

  • set:设置当前状态,并为present赋值。
  • reset:重置当前状态,清除pastfuture数组(撤消/重做历史记录),并为present赋值。
  • undo:撤消到上一个状态(通过past数组的元素)。
  • redo:重做到下一个状态(通过future数组的元素)。
  • canUndo:如果可以执行撤消操作(past数组不为空),则等于true
  • canRedo:如果可以执行重做操作,则等于truefuture数组不为空)。

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

在我们的 post 编辑器中实现 Undo/Redo

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

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

更换为useUndo挂钩,如下所示。我们将默认状态设置为''。我们还将状态保存到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>

It is important that <button> elements in a <form> element have a type attribute defined. If the type attribute is not defined, buttons are assumed to be type="submit", which means that they will trigger the onSubmit handler function when clicked.

现在,在输入文本后,我们可以按“撤消”一次删除一个字符,然后按“重做”再次添加字符。接下来,我们将实现去 Bouncing,这意味着我们的更改只会在一段时间后添加到 undo history,而不是在输入的每个字符之后。

用挂钩去抖动

正如我们在上一节中所看到的,当我们按下“撤消”时,它一次撤消一个字符。有时,我们不想在撤销历史记录中存储所有更改。为了避免存储每一个更改,我们需要实现去 Bouncing,这意味着将我们的content存储到 undo history 的函数只会在一定时间后调用。

use-debounce库提供了useDebounce挂钩,可用于以下简单值:

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

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

然而,对于我们的用例来说,这是不够的。我们需要取消公告回调,以便结合use-undo实现取消公告。use-debounce库还提供useDebouncedCallback挂钩,挂钩的使用方式如下:

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

现在,如果我们调用debouncedSet('text'),则text值将在1000毫秒(1秒)后更新。如果多次调用debouncedSet,则每次都会重置超时,因此只有在debouncedSet函数不再调用1000ms 后,才会调用setText函数。接下来,我们将继续在 post editor 中实现去 Bouncing。

在我们的博文编辑中取消公告更改

现在我们已经了解了去 Bouncing,我们将结合 post editor 中的 Undo 挂钩来实现它,如下所示:

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

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

如果我们现在在编辑器中键入一些文本,我们可以看到“撤消”按钮只会在一段时间后激活。然后看起来是这样的:

Undo button activated after typing some text

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

Going back in time using the Undo button

正如我们所看到的,撤消/重做和取消抖动现在工作得非常好!

示例代码

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

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

寻找其他挂钩

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

为了让您了解还有哪些其他挂钩,社区挂钩提供了以下特性。我们现在列出社区提供的几个更有趣的挂钩。当然,还有更多的挂钩可以找到:

总结

在本章中,我们首先了解了react-hookedup图书馆。我们在博客应用中使用这个库简化了挂钩的输入处理。然后,我们研究了如何使用挂钩实现各种 React 生命周期。接下来,我们介绍了各种有用的挂钩,如usePrevious挂钩、间隔/超时挂钩、在线状态挂钩、数据操作挂钩以及焦点和悬停挂钩。之后,我们通过不在手机上渲染某些组件,讨论了使用挂钩的响应性设计。最后,我们学习了如何使用挂钩实现撤销/重做功能和解除绑定。

使用社区挂钩是一项非常重要的技能,因为 React 只提供了一些现成的挂钩。在实际应用中,您可能会使用社区提供的来自各种库和框架的许多挂钩。我们还了解了各种社区挂钩,它们将使我们在编写 React 应用时的生活变得更加轻松。

在下一章中,我们将深入了解挂钩的规则,在我们开始编写自己的挂钩之前,必须了解这些规则。

问题

为了重述我们在本章学到的知识,请尝试回答以下问题:

  1. 我们可以使用哪个挂钩来简化输入字段处理?
  2. 如何使用效果挂钩实现componentDidMountcomponentWillUnmount生命周期?
  3. 我们如何使用挂钩来获取this.setState()的行为?

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

  5. 我们可以使用哪些挂钩来简化处理公共数据结构?
  6. 什么时候我们应该使用带有挂钩的响应式设计,而不是简单地使用 CSS 媒体查询?
  7. 我们可以使用哪个挂钩实现撤销/重做功能?
  8. 什么是去 Bouncing?我们为什么要这样做?
  9. 我们可以用哪种挂钩去抖动?

进一步阅读

如果您对我们在本章中所学概念的更多信息感兴趣,请阅读以下阅读材料: