六、实现请求和 ReactSuspence

在前面的章节中,我们学习了如何使用 React 上下文作为手动传递道具的替代方法。我们学习了上下文提供者、使用者,以及如何使用挂钩作为上下文使用者。接下来,我们学习了控制反转作为上下文的替代方法。最后,我们使用博客应用中的上下文实现了主题和全局状态。

在本章中,我们将设置一个简单的后端服务器,它将使用json-server工具从JavaScript 对象表示法JSON文件)生成。然后,我们将通过结合使用效果挂钩和状态挂钩来实现请求资源。接下来,我们将使用axiosreact-request-hook库执行同样的操作。最后,我们将使用React.memo防止不必要的重新渲染,并通过使用 React Suspense 延迟加载组件。

本章将介绍以下主题:

  • 使用挂钩请求资源
  • 使用React.memo防止不必要的重新渲染
  • 使用 React-suspence 实现延迟加载

技术要求

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

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

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

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 for you to be able to learn and understand properly. However, if you run into any issues, you can always refer to the code example.

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

使用挂钩请求资源

在本节中,我们将学习如何使用挂钩从服务器请求资源。首先,我们将只使用 JavaScriptfetch函数和useEffect/useState挂钩来实现请求。然后,我们将学习如何使用axios库和react-request-hook组合来请求资源。

设置虚拟服务器

在实现请求之前,我们需要创建一个后端服务器。由于我们目前主要关注用户界面,我们将设置一个虚拟服务器,它将允许我们测试请求。我们将使用json-server工具从 JSON 文件创建完整的表示性状态传输RESTAPI。

创建 db.json 文件

为了能够使用json-server工具,首先我们需要创建一个db.json文件,该文件将包含服务器的完整数据库。json-server工具将允许您进行以下操作:

  • GET请求,从文件中获取数据
  • POST请求,向文件中插入新数据
  • PUTPATCH请求,调整现有数据
  • DELETE请求,删除数据

对于所有修改动作(POSTPUTPATCHDELETE),工具会自动保存更新后的文件。

我们可以使用现有的 posts 结构,我们将其定义为 posts reducer 的默认状态。但是,我们需要确保提供一个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 数组存储在posts键下,用户数组存储在users键下,主题数组存储在themes键下。

让我们开始创建用作后端服务器数据库的 JSON 文件:

  1. 在应用文件夹的根目录中创建一个新的server/目录。
  2. 创建一个包含以下内容的server/db.json文件。我们可以使用简化器挂钩中的现有状态。但是,由于这是一个数据库,我们需要为每个元素提供一个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 文件作为数据库,该工具将为我们创建一个完整的 RESTAPI。

安装 json 服务器工具

现在,我们将使用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查看我们的 post 对象:

Our simple JSON server working and serving a post!

正如我们所见,该工具为我们从数据库 JSON 文件创建了一个完整的 RESTAPI!

配置 package.json

接下来,除了我们的客户机(通过webpack-dev-server运行)之外,我们还需要调整package.json文件,以便启动服务器。

让我们开始调整package.json文件:

  1. 首先,我们创建一个名为start:server的新包脚本,将其插入package.json文件的scripts部分。我们还确保更改端口,使其不会在与客户端相同的端口上运行:
    "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工具定义以下路由:https://github.com/typicode/json-server#routes

我们还可以通过创建一个routes.json文件来定义我们自己的路由,在该文件中我们可以将现有路由重写为其他路由:https://github.com/typicode/json-server#add-定制路线

对于我们的博客应用,我们将定义一个自定义路由:/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。这将返回一个用户对象;因此,登录成功!

我们可以在浏览器中看到以文本形式返回的用户对象:

Accessing our custom route directly in the browser

正如我们所看到的,访问我们的自定义路由是可行的!我们现在可以使用它来登录用户。

示例代码

示例代码可在Chapter06/chapter6_1文件夹中找到。

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

使用 Effect 和 State/Reducer 挂钩实现请求

在使用库使用挂钩实现请求之前,我们将手动实现它们,使用一个 Effect 挂钩触发请求,使用 State/Reducer 挂钩存储结果。

具有有效和状态挂钩的请求

首先,我们将从服务器请求主题,而不是硬编码主题列表。

让我们使用效果挂钩和状态挂钩实现请求主题:

  1. src/ChangeTheme.js文件中,调整 Reactimport语句以导入useEffectuseState挂钩:
import React, { useEffect, useState } from 'react'
  1. 删除THEMES常数,该常数为以下所有代码:
const THEMES = [
    { primaryColor: 'deepskyblue', secondaryColor: 'coral' },
    { primaryColor: 'orchid', secondaryColor: 'mediumseagreen' }
]
  1. ChangeTheme组件中,定义一个新的useState挂钩以存储主题:
export default function ChangeTheme ({ theme, setTheme }) {
 const [ themes, setThemes ] = useState([])
  1. 然后定义一个useEffect挂钩,我们将在其中发出请求:
    useEffect(() => {
  1. 在这个挂钩中,我们使用fetch请求一个资源;在这种情况下,我们请求/api/themes
        fetch('/api/themes')
  1. Fetch 使用 Promise API;因此,我们可以使用.then()来处理结果。首先,我们必须将结果解析为 JSON:
            .then(result => result.json())
  1. 最后,我们使用请求中的主题数组调用setThemes
            .then(themes => setThemes(themes))

We can also shorten the preceding function to .then(setThemes), as we are only passing down the themes argument from .then().

  1. 现在,这个效果挂钩应该只在组件挂载时触发,所以我们将一个空数组作为第二个参数传递给useEffect。这确保了效果挂钩没有依赖项,因此只有在组件挂载时才会触发:
    }, [])
  1. 现在,剩下要做的就是用挂钩中的themes值替换THEMES常量:
            {themes.map(t =>

如我们所见,现在有三个主题可用,都是通过服务器从数据库加载的:

Three themes loaded from our server by using hooks!

我们的主题现在从后端服务器加载,我们可以通过挂钩继续请求帖子。

具有效果和还原挂钩的请求

我们现在将使用后端服务器请求 posts 数组,而不是将其硬编码为postsReducer的默认值。

让我们使用一个 Effect 挂钩和一个 Reducer 挂钩来实现请求 post:

  1. src/App.js中删除常量定义,该常量定义为以下所有代码:
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的新动作类型。此操作类型将用新的 posts 数组替换当前状态:
function postsReducer (state, action) {
    switch (action.type) {
 case 'FETCH_POSTS':
 return action.posts
  1. src/App.js中,定义一个新的useEffect挂钩,该挂钩位于当前挂钩之前:
    useEffect(() => {
  1. 在这个挂钩中,我们再次使用fetch来请求资源;在这种情况下,我们请求/api/posts
        fetch('/api/posts')
            .then(result => result.json())
  1. 最后,我们使用请求中的posts数组发送FETCH_POSTS操作:
            .then(posts => dispatch({ type: 'FETCH_POSTS', posts }))
  1. 现在,这个效果挂钩应该只在组件挂载时触发,所以我们将一个空数组作为第二个参数传递给useEffect
    }, [])

正如我们所看到的,帖子现在被服务器请求了!我们可以查看 DevTools 网络选项卡以查看请求:

Posts being requested from our server!

现在正在从后端服务器请求帖子。在下一节中,我们将使用axiosreact-request-hook从服务器请求资源。

示例代码

示例代码可在Chapter06/chapter6_2文件夹中找到。

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

使用 axios 和 react 请求挂钩

在上一节中,我们使用一个 Effect 挂钩来触发请求,使用 Reducer/State 挂钩来更新状态,使用请求的结果。我们可以使用axiosreact-request-hook库来轻松地使用挂钩实现请求,而不是像这样手动实现请求。

建立图书馆

在开始使用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/'
})

In the config for our axios instance, we can also define other options, such as a default timeout for requests, or custom headers. For more information, check out the axios documentation: https://github.com/axios/axios#axioscreateconfig.

  1. 最后,我们用<RequestProvider>组件包装<App />组件。删除以下代码行:
ReactDOM.render(<App />, document.getElementById('root'));

将其替换为以下代码:

ReactDOM.render(
    <RequestProvider value={axiosInstance}>
        <App />
    </RequestProvider>,
    document.getElementById('root')
)

现在,我们的应用可以使用资源挂钩了!

使用 useResource 挂钩

处理请求的一种更强大的方法是使用axiosreact-request-hook库。使用这些库,我们可以访问可以取消单个请求甚至清除所有挂起请求的功能。此外,使用这些库可以更容易地处理错误和加载状态。

我们现在将实现useResource挂钩,以便从服务器请求主题:

  1. src/ChangeTheme.js中,从react-request-hook库导入useResource挂钩:
import { useResource } from 'react-request-hook'
  1. 移除先前定义的状态和效果挂钩。

  2. 然后,我们在ChangeTheme组件中定义一个useResource挂钩。挂钩返回一个值和一个 getter 函数。调用 getter 函数将请求资源:

export default function ChangeTheme ({ theme, setTheme }) {
 const [ themes, getThemes ] = useResource(() => ({

Here, we are using the shorthand syntax for () => { return { } }, which is () => ({ }). Using this shorthand syntax allows us to concisely write functions that only return an object.

  1. 在这个挂钩中,我们传递一个函数,该函数返回一个包含请求信息的对象:
        url: '/themes',
        method: 'get'
    }))

With axios, we only need to pass /themes as the url, because we already defined the baseURL, which contains /api/.

  1. 资源挂钩返回一个具有data值的对象、一个isLoading布尔值、一个error对象和一个cancel函数以取消挂起的请求。现在,我们从themes对象中取出data值和isLoading布尔值:
    const { data, isLoading } = themes
  1. 然后,我们定义了一个useEffect挂钩来触发getThemes函数,我们只希望它在组件挂载时触发一次;因此,我们传递一个空数组作为第二个参数:
    useEffect(getThemes, [])
  1. 此外,我们使用isLoading标志在等待服务器响应时显示加载消息:
            {isLoading && ' Loading themes...'}
  1. 最后,我们将themes值重命名为useResource挂钩返回的data值,并添加条件检查以确保data值已经可用:
            {data && data.map(t =>

如果我们现在看看我们的应用,我们可以看到加载主题。。。消息会在很短的时间内显示出来,然后我们数据库中的主题就会显示出来!我们现在可以继续使用资源挂钩请求帖子了。

使用带简化器挂钩的 useResource

useResource挂钩已经处理了我们请求结果的状态,因此我们不需要额外的useState挂钩来存储状态。但是,如果我们已经有一个简化器挂钩,我们可以将其与useResource挂钩结合使用。

我们现在将在我们的应用中结合简化器挂钩来实现useResource挂钩:

  1. src/App.js中,从react-request-hook库导入useResource挂钩:
import { useResource } from 'react-request-hook'
  1. 移除之前定义的使用fetch请求/api/postsuseEffect挂钩。
  2. 定义一个新的useResource挂钩,我们请求/posts
    const [ posts, getPosts ] = useResource(() => ({
        url: '/posts',
        method: 'get'
    }))
  1. 定义一个新的useEffect挂钩,它只调用getPosts
    useEffect(getPosts, [])
  1. 最后,定义一个useEffect挂钩,在检查数据是否已经存在后,该挂钩发出FETCH_POSTS动作:
    useEffect(() => {
        if (posts && posts.data) {
            dispatch({ type: 'FETCH_POSTS', posts: posts.data })
        }
  1. 我们确保每次posts对象更新时都会触发此效果挂钩:
    }, [posts])

现在,当我们获取新帖子时,将发出一个FETCH_POSTS动作。接下来,我们继续处理请求期间的错误。

处理错误状态

我们已经处理了ChangeTheme组件中的加载状态。现在,我们将实现 POST 的错误状态。

让我们开始处理 POST 的错误状态:

  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中,调整我们简化器吊钩的默认状态:
    const [ state, dispatch ] = useReducer(appReducer, { user: '', posts: [], error: '' })
  1. error值从state对象中拉出:
    const { user, error } = state
  1. 现在,我们可以调整处理来自posts资源的新数据的现有效果挂钩,在出现错误的情况下发送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,将显示错误:

Displaying an error when the request fails!

正如我们所看到的,由于服务器未运行,我们的应用中会显示 Failed to fetch posts 错误。现在,我们可以通过请求实现后期创建。

实施岗位创建

现在我们已经很好地掌握了如何从 API 请求数据,我们将使用useResource挂钩来创建新数据。

让我们开始使用资源挂钩实现后期创建:

  1. 编辑src/post/CreatePost.js,导入useResource挂钩:
import { useResource } from 'react-request-hook'
  1. 然后,定义一个新的资源挂钩,在其他挂钩下面,但在我们的处理函数定义之前。在这里,我们将方法设置为post(创建新数据),并将数据从createPost函数传递到请求配置:
    const [ , createPost ] = useResource(({ title, content, author }) => ({
        url: '/posts',
        method: 'post',
        data: { title, content, author }
    }))

Here, we are using a shorthand syntax for array destructuring: we are ignoring the first element of the array, by not specifying a value name. Instead of writing const [ post, createPost ], and then not using post, we just put a comma, as follows: const [  , createPost ].

  1. 现在,我们可以在handleCreate处理函数中使用createPost函数。我们确保保持对dispatch函数的调用,以便在等待服务器响应时立即插入新的 post 客户端。添加的代码以粗体突出显示:
    function handleCreate () {
 createPost({ title, content, author: user })
        dispatch({ type: 'CREATE_POST', title, content, author: user })
    }

Please note that, in this simple example, we do not expect, or handle the failure of post creations. In this case, we dispatch the action even before the request completes. However, when implementing login, we are going to handle error states from the request, in order to check whether the user was logged in successfully. It is best practice to always handle error states in real-world applications.

  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])

Please note that, in this simple example, we do not expect, or handle the failure of registrations. In this case, we dispatch the action only after the successful creation of the user. However, when implementing login, we are going to handle error states from the request, in order to check whether the user was logged in successfully. It is best practice to always handle error states in real-world applications.

  1. 最后,我们调整表单提交处理程序以调用register函数,而不是直接调度操作:
        <form onSubmit={e => { e.preventDefault(); register(username, password) }}>

现在,如果我们输入用户名和密码,然后按 Register,一个新用户将被插入到我们的db.json文件中,就像以前一样,我们将登录。现在我们继续通过资源挂钩实现登录。

实现登录

最后,我们将通过使用自定义路由的请求实现登录。完成此操作后,我们的博客应用将完全连接到服务器。

让我们开始实施登录:

  1. 首先编辑src/user/Login.js并导入useEffectuseResource挂钩:
import React, { useState, useContext, useEffect } from 'react'
import { useResource } from 'react-request-hook'
  1. 我们定义了一个新的状态挂钩,该挂钩将存储一个布尔值,以检查登录是否失败:
    const [ loginFailed, setLoginFailed ] = useState(false)
  1. 然后,我们为密码字段定义了一个新的状态挂钩,因为我们以前没有处理它:
    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. 现在,我们可以在状态挂钩下面定义我们的资源挂钩,在那里我们将通过usernamepassword/login路由。由于我们将它们作为 URL 的一部分传递,因此我们需要确保首先对它们进行正确编码:
    const [ user, login ] = useResource((username, password) => ({
        url: `/login/${encodeURI(username)}/${encodeURI(password)}`,
        method: 'get'
    }))

Please note that it is not secure to send the password in cleartext via a GET request. We only do this for the sake of simplicity when configuring our dummy server. In a real world application, use a POST request for login instead and send the password as part of the POST data. Also make sure to use Hypertext Transfer Protocol Secure (HTTPS) so that the POST data will be encrypted.

  1. 接下来,我们定义一个 Effect 挂钩,如果请求成功完成,它将调度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. 我们确保每当来自资源挂钩的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>}

如我们所见,输入错误的用户名或密码(或没有密码)将导致错误,而输入正确的用户名/密码组合将使我们登录:

Displaying an error message when the login failed

现在,我们的应用已完全连接到后端服务器!

示例代码

示例代码可在Chapter06/chapter6_3文件夹中找到。

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

使用 React.memo 防止不必要的重新渲染

对于类组件,我们有shouldComponentUpdate,如果道具没有改变,这将阻止组件重新渲染。

对于功能组件,我们可以使用高阶组件React.memo进行同样的操作。React.memo记忆结果,这意味着它将记住上次渲染的结果,如果道具没有改变,它将跳过重新渲染组件:

const SomeComponent = () => ...

export default React.memo(SomeComponent)

默认情况下,React.memo的行为类似于shouldComponentUpdate的默认定义,它只会粗略地比较道具对象。如果我们想做一个特殊的比较,我们可以将一个函数作为第二个参数传递给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的函数在道具相等时返回true,因此不应该更新,这与shouldComponentUpdate的工作方式相反!在了解了React.memo之后,让我们通过为 Post 组件实现React.memo在实践中进行尝试。

为 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(在大多数浏览器上:右键单击页面上的“检查”)。转到 Console 选项卡,您应该会看到两次输出,因为我们正在呈现两篇文章:

The debug output when rendering two posts

  1. 到目前为止,一切顺利。现在,让我们尝试登录,看看会发生什么:

Posts re-rendering after logging in

正如我们所看到的,Post 组件在登录后不必要地重新渲染,尽管它们的道具没有改变。我们可以使用React.memo来防止这种情况,如下所示:

  1. 编辑src/post/Post.js,删除功能定义的导出默认部分(粗体标记):
export default function Post ({ title, content, author }) {
  1. 然后,在文件的底部,在用React.memo()包装后导出 Post 组件:
export default React.memo(Post)
  1. 现在,刷新页面并再次登录。我们可以看到这两篇文章被渲染,从而生成初始调试输出。但是,现在登录不会导致 Post 组件重新渲染!

如果我们想对职位是否相等进行自定义检查,我们可以比较titlecontentauthor,如下所示:

export default React.memo(Post,
    (prev, next) => prev.title === next.title && prev.content === next.content && prev.author === next.author
)

在我们的例子中,这样做也会产生同样的效果,因为 React 默认情况下已经对所有道具进行了粗略的比较。只有当我们需要比较深层对象时,或者当我们想忽略某些道具中的更改时,此函数才有用。请注意,我们不应该过早地优化代码。重新渲染可以很好,因为 React 是智能的,如果没有任何更改,则不会绘制到浏览器。因此,优化所有重新渲染可能有些过分,除非某个情况已被确定为性能瓶颈。

示例代码

示例代码可在Chapter06/chapter6_4文件夹中找到。

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

用 React-suspence 实现延迟加载

ReactSuspence 允许我们让组件在渲染之前等待。目前,React Suspense 仅允许我们使用React.lazy动态加载组件。将来,Suspence 将支持其他用例,例如数据获取。

React.lazy是性能优化的另一种形式。它允许我们动态加载组件,以减少捆绑包的大小。有时,我们希望避免在初始渲染期间加载所有组件,而只在需要时请求某些组件。

例如,如果我们的博客有一个成员区域,我们只需要在用户登录后加载它。这样做将减少只访问我们的博客阅读博客文章的客人的捆绑包大小。为了了解 ReactSuspence,我们将在我们的博客应用中惰性地加载Logout组件。

悬念

首先,我们必须指定一个加载指示器,当我们的延迟加载组件正在加载时将显示该指示器。在我们的示例中,我们将用 ReactSuspence 来包装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'))

The import() function dynamically loads the Logout component from the Logout.js file. In contrast to the static import statement, this function only gets called when React.lazy triggers it, which means it will only be imported when the component is needed.

如果我们想看到延迟加载的效果,我们可以在 Google Chrome 中设置网络节流以降低 3G 速度:

Setting Network Throttling to Slow 3G in Google Chrome In Firefox, we can do the same by setting Network Throttling to GPRS. Safari unfortunately does not offer such a feature right now, but we can use the Network Link Conditioner tool from Apple's "Hardware IO tools": https://developer.apple.com/download/more/

如果我们现在刷新页面,然后登录,我们可以首先看到加载。。。消息,然后显示Logout组件。如果我们查看网络日志,我们可以看到Logout组件是通过网络请求的:

The Logout component being loaded via the network

正如我们所看到的,Logout组件现在是延迟加载的,这意味着只有在需要时才会请求它。

示例代码

示例代码可在Chapter06/chapter6_5文件夹中找到。

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

总结

在本章中,我们首先学习了如何从 JSON 文件设置 API 服务器。然后,我们学习了如何使用 Effect 和 State/Reducer 挂钩请求资源。接下来,我们学习了如何使用axiosreact-request-hook库请求资源。最后,我们学习了如何使用React.memo防止不必要的重新渲染,以及如何使用 React Suspense 惰性地加载组件。

在下一章中,我们将向应用添加路由,并学习如何使用挂钩进行路由。

问题

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

  1. 如何从一个简单的 JSON 文件轻松创建一个完整的 RESTAPI?
  2. 在开发过程中使用代理访问我们的后端服务器有什么好处?
  3. 我们可以使用哪些挂钩组合来实现请求?
  4. 我们可以使用哪些库来实现请求?
  5. 我们如何使用react-request-hook处理加载状态?
  6. 我们如何使用react-request-hook处理错误?
  7. 如何防止不必要的组件重新渲染?
  8. 我们如何减少应用的捆绑大小?

进一步阅读

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