六、实现请求和 ReactSuspence
在前面的章节中,我们学习了如何使用 React 上下文作为手动传递道具的替代方法。我们学习了上下文提供者、使用者,以及如何使用挂钩作为上下文使用者。接下来,我们学习了控制反转作为上下文的替代方法。最后,我们使用博客应用中的上下文实现了主题和全局状态。
在本章中,我们将设置一个简单的后端服务器,它将使用json-server
工具从JavaScript 对象表示法(JSON文件)生成。然后,我们将通过结合使用效果挂钩和状态挂钩来实现请求资源。接下来,我们将使用axios
和react-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 。
请查看以下视频以查看代码的运行情况:
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
请求,向文件中插入新数据PUT
和PATCH
请求,调整现有数据DELETE
请求,删除数据
对于所有修改动作(POST
、PUT
、PATCH
、DELETE
),工具会自动保存更新后的文件。
我们可以使用现有的 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 文件:
- 在应用文件夹的根目录中创建一个新的
server/
目录。 - 创建一个包含以下内容的
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
工具安装并启动我们的后端服务器:
- 首先,我们将通过
npm
安装json-server
工具:
> npm install --save json-server
- 现在,我们可以通过调用以下命令启动后端服务器:
>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
文件:
- 首先,我们创建一个名为
start:server
的新包脚本,将其插入package.json
文件的scripts
部分。我们还确保更改端口,使其不会在与客户端相同的端口上运行:
"scripts": {
"start:server": "npx json-server --watch server/db.json --port 4000",
"start": "react-scripts start",
- 然后,我们将
start
脚本重命名为start:client
:
"scripts": {
"start:server": "npx json-server --watch server/db.json",
"start:client": "react-scripts start",
- 接下来,我们安装一个名为
concurrently
的工具,让我们同时启动服务器和客户端:
> npm install --save concurrently
- 现在,我们可以使用
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/
的代理。
现在,让我们配置代理:
- 首先,我们必须安装
http-proxy-middleware
软件包:
> npm install --save http-proxy-middleware
- 然后,我们创建一个新的
src/setupProxy.js
文件,内容如下:
const proxy = require('http-proxy-middleware')
module.exports = function (app) {
app.use(proxy('/api', {
- 接下来,我们必须定义代理的目标,它将是后端服务器,运行在
http://localhost:4000
:
target: 'http://localhost:4000',
- 最后,我们必须定义一个路径重写规则,在将请求转发到服务器之前删除
/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
查询,以便找到具有给定用户名和密码组合的用户。
我们现在将为我们的应用定义自定义登录路径:
- 创建具有以下内容的新
server/routes.json
文件:
{
"/login/:username/:password": "/users?username=:username&password=:password"
}
- 然后,调整
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 挂钩存储结果。
具有有效和状态挂钩的请求
首先,我们将从服务器请求主题,而不是硬编码主题列表。
让我们使用效果挂钩和状态挂钩实现请求主题:
- 在
src/ChangeTheme.js
文件中,调整 Reactimport
语句以导入useEffect
和useState
挂钩:
import React, { useEffect, useState } from 'react'
- 删除
THEMES
常数,该常数为以下所有代码:
const THEMES = [
{ primaryColor: 'deepskyblue', secondaryColor: 'coral' },
{ primaryColor: 'orchid', secondaryColor: 'mediumseagreen' }
]
- 在
ChangeTheme
组件中,定义一个新的useState
挂钩以存储主题:
export default function ChangeTheme ({ theme, setTheme }) {
const [ themes, setThemes ] = useState([])
- 然后定义一个
useEffect
挂钩,我们将在其中发出请求:
useEffect(() => {
- 在这个挂钩中,我们使用
fetch
请求一个资源;在这种情况下,我们请求/api/themes
:
fetch('/api/themes')
- Fetch 使用 Promise API;因此,我们可以使用
.then()
来处理结果。首先,我们必须将结果解析为 JSON:
.then(result => result.json())
- 最后,我们使用请求中的主题数组调用
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()
.
- 现在,这个效果挂钩应该只在组件挂载时触发,所以我们将一个空数组作为第二个参数传递给
useEffect
。这确保了效果挂钩没有依赖项,因此只有在组件挂载时才会触发:
}, [])
- 现在,剩下要做的就是用挂钩中的
themes
值替换THEMES
常量:
{themes.map(t =>
如我们所见,现在有三个主题可用,都是通过服务器从数据库加载的:
Three themes loaded from our server by using hooks!
我们的主题现在从后端服务器加载,我们可以通过挂钩继续请求帖子。
具有效果和还原挂钩的请求
我们现在将使用后端服务器请求 posts 数组,而不是将其硬编码为postsReducer
的默认值。
让我们使用一个 Effect 挂钩和一个 Reducer 挂钩来实现请求 post:
- 从
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' }
]
- 用空数组替换
useReducer
函数中的defaultPosts
常量:
const [ state, dispatch ] = useReducer(appReducer, { user: '', posts: [] })
- 在
src/reducers.js
中,在postsReducer
函数中定义一个名为FETCH_POSTS
的新动作类型。此操作类型将用新的 posts 数组替换当前状态:
function postsReducer (state, action) {
switch (action.type) {
case 'FETCH_POSTS':
return action.posts
- 在
src/App.js
中,定义一个新的useEffect
挂钩,该挂钩位于当前挂钩之前:
useEffect(() => {
- 在这个挂钩中,我们再次使用
fetch
来请求资源;在这种情况下,我们请求/api/posts
:
fetch('/api/posts')
.then(result => result.json())
- 最后,我们使用请求中的
posts
数组发送FETCH_POSTS
操作:
.then(posts => dispatch({ type: 'FETCH_POSTS', posts }))
- 现在,这个效果挂钩应该只在组件挂载时触发,所以我们将一个空数组作为第二个参数传递给
useEffect
:
}, [])
正如我们所看到的,帖子现在被服务器请求了!我们可以查看 DevTools 网络选项卡以查看请求:
Posts being requested from our server!
现在正在从后端服务器请求帖子。在下一节中,我们将使用axios
和react-request-hook
从服务器请求资源。
示例代码
示例代码可在Chapter06/chapter6_2
文件夹中找到。
只需运行npm install
即可安装所有依赖项,npm start
即可启动应用;然后在浏览器中访问http://localhost:3000
(如果它没有自动打开)。
使用 axios 和 react 请求挂钩
在上一节中,我们使用一个 Effect 挂钩来触发请求,使用 Reducer/State 挂钩来更新状态,使用请求的结果。我们可以使用axios
和react-request-hook
库来轻松地使用挂钩实现请求,而不是像这样手动实现请求。
建立图书馆
在开始使用axios
和react-request-hook
之前,我们必须先设置axios
实例和RequestProvider
组件。
让我们开始设置库:
- 首先,我们安装库:
>npm install --save react-request-hook axios
- 然后我们在
src/index.js
中导入:
import { RequestProvider } from 'react-request-hook'
import axios from 'axios'
- 现在,我们定义一个
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.
- 最后,我们用
<RequestProvider>
组件包装<App />
组件。删除以下代码行:
ReactDOM.render(<App />, document.getElementById('root'));
将其替换为以下代码:
ReactDOM.render(
<RequestProvider value={axiosInstance}>
<App />
</RequestProvider>,
document.getElementById('root')
)
现在,我们的应用可以使用资源挂钩了!
使用 useResource 挂钩
处理请求的一种更强大的方法是使用axios
和react-request-hook
库。使用这些库,我们可以访问可以取消单个请求甚至清除所有挂起请求的功能。此外,使用这些库可以更容易地处理错误和加载状态。
我们现在将实现useResource
挂钩,以便从服务器请求主题:
- 在
src/ChangeTheme.js
中,从react-request-hook
库导入useResource
挂钩:
import { useResource } from 'react-request-hook'
-
移除先前定义的状态和效果挂钩。
-
然后,我们在
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.
- 在这个挂钩中,我们传递一个函数,该函数返回一个包含请求信息的对象:
url: '/themes',
method: 'get'
}))
With axios
, we only need to pass /themes
as the url
, because we already defined the baseURL
, which contains /api/
.
- 资源挂钩返回一个具有
data
值的对象、一个isLoading
布尔值、一个error
对象和一个cancel
函数以取消挂起的请求。现在,我们从themes
对象中取出data
值和isLoading
布尔值:
const { data, isLoading } = themes
- 然后,我们定义了一个
useEffect
挂钩来触发getThemes
函数,我们只希望它在组件挂载时触发一次;因此,我们传递一个空数组作为第二个参数:
useEffect(getThemes, [])
- 此外,我们使用
isLoading
标志在等待服务器响应时显示加载消息:
{isLoading && ' Loading themes...'}
- 最后,我们将
themes
值重命名为useResource
挂钩返回的data
值,并添加条件检查以确保data
值已经可用:
{data && data.map(t =>
如果我们现在看看我们的应用,我们可以看到加载主题。。。消息会在很短的时间内显示出来,然后我们数据库中的主题就会显示出来!我们现在可以继续使用资源挂钩请求帖子了。
使用带简化器挂钩的 useResource
useResource
挂钩已经处理了我们请求结果的状态,因此我们不需要额外的useState
挂钩来存储状态。但是,如果我们已经有一个简化器挂钩,我们可以将其与useResource
挂钩结合使用。
我们现在将在我们的应用中结合简化器挂钩来实现useResource
挂钩:
- 在
src/App.js
中,从react-request-hook
库导入useResource
挂钩:
import { useResource } from 'react-request-hook'
- 移除之前定义的使用
fetch
请求/api/posts
的useEffect
挂钩。 - 定义一个新的
useResource
挂钩,我们请求/posts
:
const [ posts, getPosts ] = useResource(() => ({
url: '/posts',
method: 'get'
}))
- 定义一个新的
useEffect
挂钩,它只调用getPosts
:
useEffect(getPosts, [])
- 最后,定义一个
useEffect
挂钩,在检查数据是否已经存在后,该挂钩发出FETCH_POSTS
动作:
useEffect(() => {
if (posts && posts.data) {
dispatch({ type: 'FETCH_POSTS', posts: posts.data })
}
- 我们确保每次
posts
对象更新时都会触发此效果挂钩:
}, [posts])
现在,当我们获取新帖子时,将发出一个FETCH_POSTS
动作。接下来,我们继续处理请求期间的错误。
处理错误状态
我们已经处理了ChangeTheme
组件中的加载状态。现在,我们将实现 POST 的错误状态。
让我们开始处理 POST 的错误状态:
- 在
src/reducers.js
中,用新的动作类型POSTS_ERROR
定义一个新的errorReducer
函数:
function errorReducer (state, action) {
switch (action.type) {
case 'POSTS_ERROR':
return 'Failed to fetch posts'
default:
return state
}
}
- 将
errorReducer
函数添加到我们的appReducer
函数中:
export default function appReducer (state, action) {
return {
user: userReducer(state.user, action),
posts: postsReducer(state.posts, action),
error: errorReducer(state.error, action)
}
}
- 在
src/App.js
中,调整我们简化器吊钩的默认状态:
const [ state, dispatch ] = useReducer(appReducer, { user: '', posts: [], error: '' })
- 将
error
值从state
对象中拉出:
const { user, error } = state
- 现在,我们可以调整处理来自
posts
资源的新数据的现有效果挂钩,在出现错误的情况下发送POSTS_ERROR
操作:
useEffect(() => {
if (posts && posts.error) {
dispatch({ type: 'POSTS_ERROR' })
}
if (posts && posts.data) {
dispatch({ type: 'FETCH_POSTS', posts: posts.data })
}
}, [posts])
- 最后,我们在
PostList
组件前显示错误消息:
{error && <b>{error}</b>}
<PostList />
如果我们现在只启动客户端(通过npm run start:client
,将显示错误:
Displaying an error when the request fails!
正如我们所看到的,由于服务器未运行,我们的应用中会显示 Failed to fetch posts 错误。现在,我们可以通过请求实现后期创建。
实施岗位创建
现在我们已经很好地掌握了如何从 API 请求数据,我们将使用useResource
挂钩来创建新数据。
让我们开始使用资源挂钩实现后期创建:
- 编辑
src/post/CreatePost.js
,导入useResource
挂钩:
import { useResource } from 'react-request-hook'
- 然后,定义一个新的资源挂钩,在其他挂钩下面,但在我们的处理函数定义之前。在这里,我们将方法设置为
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 ]
.
- 现在,我们可以在
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.
- 请注意,当我们现在插入帖子时,帖子将首先位于列表的开头;但是,刷新后,它将位于列表的末尾。不幸的是,我们的服务器在列表的末尾插入了新帖子。因此,在从服务器获取帖子之后,我们将颠倒顺序。编辑
src/App.js
,并调整以下代码:
if (posts && posts.data) {
dispatch({ type: 'FETCH_POSTS', posts: posts.data.reverse() })
}
现在,通过服务器插入一篇新文章就可以了,我们可以继续实现注册了!
实施注册
接下来,我们将实施注册,这将以非常类似于创建帖子的方式工作。
让我们开始实施注册:
- 首先,导入
src/user/Register.js
中的useEffect
和useResource
挂钩:
import React, { useState, useContext, useEffect } from 'react'
import { useResource } from 'react-request-hook'
- 然后,定义一个新的
useResource
挂钩,在其他挂钩的下面,在处理程序运行之前。与我们在后期创建中所做的不同,我们现在还希望存储生成的user
对象:
const [ user, register ] = useResource((username, password) => ({
url: '/users',
method: 'post',
data: { username, password }
}))
- 接下来,在
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.
- 最后,我们调整表单提交处理程序以调用
register
函数,而不是直接调度操作:
<form onSubmit={e => { e.preventDefault(); register(username, password) }}>
现在,如果我们输入用户名和密码,然后按 Register,一个新用户将被插入到我们的db.json
文件中,就像以前一样,我们将登录。现在我们继续通过资源挂钩实现登录。
实现登录
最后,我们将通过使用自定义路由的请求实现登录。完成此操作后,我们的博客应用将完全连接到服务器。
让我们开始实施登录:
- 首先编辑
src/user/Login.js
并导入useEffect
和useResource
挂钩:
import React, { useState, useContext, useEffect } from 'react'
import { useResource } from 'react-request-hook'
- 我们定义了一个新的状态挂钩,该挂钩将存储一个布尔值,以检查登录是否失败:
const [ loginFailed, setLoginFailed ] = useState(false)
- 然后,我们为密码字段定义了一个新的状态挂钩,因为我们以前没有处理它:
const [ password, setPassword ] = useState('')
- 现在,我们在
handleUsername
函数下面为密码字段定义一个处理函数:
function handlePassword (evt) {
setPassword(evt.target.value)
}
- 接下来,我们处理
input
字段中的值更改:
<input type="password" value={password} onChange={handlePassword} name="login-username" id="login-username" />
- 现在,我们可以在状态挂钩下面定义我们的资源挂钩,在那里我们将通过
username
和password
到/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.
- 接下来,我们定义一个 Effect 挂钩,如果请求成功完成,它将调度
LOGIN
操作:
useEffect(() => {
if (user && user.data) {
- 由于登录路由返回空数组(登录失败)或单个用户的数组,因此我们需要检查数组是否包含至少一个元素:
if (user.data.length > 0) {
setLoginFailed(false)
dispatch({ type: 'LOGIN', username: user.data[0].username })
} else {
- 如果数组为空,我们将
loginFailed
设置为true
:
setLoginFailed(true)
}
}
- 如果我们从服务器收到错误响应,我们还将登录状态设置为失败:
if (user && user.error) {
setLoginFailed(true)
}
- 我们确保每当来自资源挂钩的
user
对象更新时,效果挂钩就会触发:
}, [user])
- 然后调整
form
的onSubmit
函数,调用login
函数:
<form onSubmit={e => { e.preventDefault(); login(username, password) }}>
- 最后,在提交按钮下方,如果
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
语句,如下所示:
- 编辑
src/post/Post.js
,组件呈现时添加以下调试输出:
export default function Post ({ title, content, author }) {
console.log('rendering Post')
- 现在,在
http://localhost:3000
打开应用,并打开 DevTools(在大多数浏览器上:右键单击页面上的“检查”)。转到 Console 选项卡,您应该会看到两次输出,因为我们正在呈现两篇文章:
The debug output when rendering two posts
- 到目前为止,一切顺利。现在,让我们尝试登录,看看会发生什么:
Posts re-rendering after logging in
正如我们所看到的,Post 组件在登录后不必要地重新渲染,尽管它们的道具没有改变。我们可以使用React.memo
来防止这种情况,如下所示:
- 编辑
src/post/Post.js
,删除功能定义的导出默认部分(粗体标记):
export default function Post ({ title, content, author }) {
- 然后,在文件的底部,在用
React.memo()
包装后导出 Post 组件:
export default React.memo(Post)
- 现在,刷新页面并再次登录。我们可以看到这两篇文章被渲染,从而生成初始调试输出。但是,现在登录不会导致 Post 组件重新渲染!
如果我们想对职位是否相等进行自定义检查,我们可以比较title
、content
和author
,如下所示:
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
组件进行延迟加载,如下所示:
- 编辑
src/user/UserBar.js
,删除Logout
组件的导入语句:
import Logout from './Logout'
- 然后,通过延迟加载定义
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 挂钩请求资源。接下来,我们学习了如何使用axios
和react-request-hook
库请求资源。最后,我们学习了如何使用React.memo
防止不必要的重新渲染,以及如何使用 React Suspense 惰性地加载组件。
在下一章中,我们将向应用添加路由,并学习如何使用挂钩进行路由。
问题
为了回顾我们在本章学到的知识,请尝试回答以下问题:
- 如何从一个简单的 JSON 文件轻松创建一个完整的 RESTAPI?
- 在开发过程中使用代理访问我们的后端服务器有什么好处?
- 我们可以使用哪些挂钩组合来实现请求?
- 我们可以使用哪些库来实现请求?
- 我们如何使用
react-request-hook
处理加载状态? - 我们如何使用
react-request-hook
处理错误? - 如何防止不必要的组件重新渲染?
- 我们如何减少应用的捆绑大小?
进一步阅读
如果您对我们在本章中探讨的概念的更多信息感兴趣,请阅读以下阅读材料:
json-server
的正式文件:https://github.com/typicode/json-server 。concurrently
的正式文件:https://github.com/kimmobrunfeldt/concurrently 。axios
的正式文件:https://github.com/axios/axios 。react-request-hook
的正式文件:https://github.com/schettino/react-request-hook 。- 创建有关配置代理的 React 应用文档:https://facebook.github.io/create-react-app/docs/proxying-api-requests-in-development#configuring-手动删除代理。
- 使用 React 挂钩获取数据:https://www.robinwieruch.de/react-hooks-fetch-data
- 何时使用
useMemo
:https://kentcdodds.com/blog/usememo-and-usecallback
版权属于:月萌API www.moonapi.com,转载请注明出处