使用 Hooks 控制 React API 调用
我很喜欢 React。但这个框架的某些方面之前让我有些不适应。其中之一就是 React 应用内部 API 调用的控制非常严格。
您见过多少次这样的场景?
你在浏览器中加载了一个 React 应用,由于你是一名前端开发者,你打开了检查器工具,查看了应用发出的 API(异步)调用。这时,你发现了一些……不对劲的地方。
该应用GET
向某个端点发出一个简单的请求,请求获取一批基本数据。通常,这些数据看起来很少(甚至根本不会)发生变化。然而……该应用却对同一个端点进行了两次、三次甚至更多次调用。而且,在每次调用中,它都会检索到完全相同的数据。
几乎每次我目睹这种情况时,我都清楚地知道为什么会发生这种情况:因为开发人员不了解如何正确控制从他们自己的应用程序启动的 API 调用!
平心而论,这是我见过的很多React 应用里极其常见的错误。它之所以如此常见,只有一个非常基本的原因:React 在指导开发者如何进行命令式调用方面做得非常糟糕。更简洁地说,React 倾向于掩盖当你需要在特定时间执行单个操作,并确保该操作仅发生一次时出现的问题。
默认情况下,React 并不想让你用命令式思维。它不断鼓励你以声明式的方式编程。需要明确的是,这通常是一件好事。但有些事情就是无法完全适应声明式模型——API 调用肯定就是其中一种情况。
这让我抓狂。因为有些 API 调用实际上应该只执行一次(或者……在非常特定的条件下)。所以,我认为,当应用反复调用相同的数据时,这是一种“性能失当”的行为——通常是在用户有机会以任何方式与数据交互之前。
阿波罗噩梦
在介绍我的解决方案之前,我想简单介绍一下 Apollo。这似乎是大多数开发者管理 GraphQL 调用时都会使用的“默认”包。这……也还好。但恕我直言,它有一个很大的缺点:它的所有默认文档都试图引导你以声明式的方式构建 API 调用。而对于许多不同的数据调用来说,这简直是愚蠢至极。(我写了一整篇文章来讨论这个问题,你可以在这里阅读:https://dev.to/bytebodger/react-s-odd-obsession-with-declarative-syntax-4k8h)
全面披露:完全可以命令式地管理你的 Apollo GraphQL 调用。但你得花很多时间翻阅他们的文档才能搞清楚如何正确操作。这快把我逼疯了。
React 的渲染周期(由协调过程驱动)对大多数开发者来说通常感觉非常“黑盒”。即使是经验丰富的 React 开发者,也很难确切地说出渲染周期何时会被调用。这就是为什么我鄙视 Apollo 的默认方法。因为 API 调用绝对是你应用的一个方面,你绝对不应该盲目地将其交给 React 协调过程的内部运作。(我写了一篇关于协调过程的完整文章。你可以在这里阅读:https://dev.to/bytebodger/react-s-render-doesn-t-render-1jc5)
所以我并不是建议你放弃 Apollo(以及它偏爱的声明式语法)。但如果你在阅读本教程的剩余部分时,想知道“为什么不直接用 Apollo?”,原因就在这里。当我编写响应式异步应用程序时,我发现将所有API 调用都交给渲染周期的变幻莫测总是不尽如人意。
只需使用 Saga
我几乎是公认的 Redux 死忠粉。(你可以在这里阅读我的完整吐槽:https://dev.to/bytebodger/the-splintering-effects-of-redux-3b4j)但我完全理解,许多 React 开发者已经完全依赖 Redux。所以,如果你的项目已经使用了 Redux,那么我可以肯定地说,你应该使用 Saga 来管理你的 API 调用。它是专门为处理“副作用”而设计的,而它首页上展示的第一个副作用就是 API 调用。
所以,如果你已经熟悉 Redux Saga,我怀疑我在这里向你展示的任何东西都无法超越这项根深蒂固的技术。用它吧。它很酷。
但是,如果你还没有“Redux 商店”怎么办?如果你不想引入 Redux 的所有内置开销,只想干净利落地管理少量 API 调用怎么办?嗯……有个好消息。你可以使用 Hooks 轻松实现这一点。
禁忌知识
好吧……我说过这很“简单”。但这并不一定意味着它显而易见。事实上,几年前我花了大量时间在网上搜索,试图找到如何在不调用 Redux 这个恶魔的情况下,正确地管理我的 API 调用。
听起来很简单,对吧?但奇怪的是,我越是寻找解决方案,就越对各种网站和博客上提供的解决方案感到恼火。所以,每当我可以选择自己的方案时,我都会详细地向你介绍我是如何管理 API 调用的。
基本设置
(在我开始之前,您可以在这里看到所有这些代码:https: //stackblitz.com/edit/react-px4ukm)
我们将从一个非常简单的 React 应用程序开始,其结构如下:
/public
/src
/common
/functions
get.axios.js
load.shared.hooks.js
/hooks
use.reservations.endpoint.js
/objects
use.js
App.js
index.js
Reservations.js
UI.js
package.json
显然,您不必使用我的文件结构。您可以根据自己的需要重新排列。此演示是用 构建的create-react-app
。同样,您显然不需要使用它。这可以在自定义 Webpack 构建中完成。我将从应用程序的顶部开始,引导您完成所有相关要点。
包.json
{
"name": "react",
"version": "0.0.0",
"private": true,
"dependencies": {
"@toolz/use-constructor": "^1.0.1",
"axios": "0.26.0",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"react-scripts": "latest"
}
}
这些都是相当标准的东西。我只想指出两个特点:
-
我使用的是自定义的
@toolz/use-constructor
NPM 包。(您可以在这里阅读所有相关信息:https://dev.to/bytebodger/constructors-in- functional-components-with-hooks-280m)如果您愿意,可以手动编写此包的功能。它只是确保我们能够以“传统”的类似构造函数的思维方式调用 API——这意味着代码只会运行一次。这就是我们要调用 API 的地方。 -
我使用这个
Axios
包来调用异步调用。你可以使用任何适合你的方法——即使你只是在进行“老式”的、简单的 JavaScript 异步调用。
index.js
大家继续往下看吧。这里没什么可看的。这只是index.js
启动新的 Create React App 时获得的默认文件。它实际上做的就是调用<App/>
。
App.js
import React from 'react';
import { loadSharedHooks } from './common/functions/load.shared.hooks';
import { UI } from './UI';
export default function App() {
loadSharedHooks();
return <UI/>;
}
我通常几乎不把真正的“逻辑”放在 中App.js
。它只是作为真正应用的启动点。在这个组件中,我只是调用<UI/>
,然后……我又调用loadSharedHooks()
。在这里,我使用了一种允许我真正在任何/所有组件之间共享全局状态的方法,只使用核心 React 和 Hooks。没有 Redux。没有其他第三方共享状态包。只有……React Hooks。(您可以在这篇文章中阅读有关此方法的所有内容:https://dev.to/bytebodger/hacking-react-hooks-shared-global-state-553b)
/common/functions/load.shared.hooks.js
import { use } from '../objects/use';
import { useReservationsEndpoint } from '../hooks/use.reservations.endpoint';
export const loadSharedHooks = () => {
use.reservationsEndpoint = useReservationsEndpoint();
};
这是一个非常简单的函数。首先,我为每个要访问的端点创建一个自定义 Hook。然后,我将端点的单个实例use
(“单例”)放入对象中。这将 API 调用置于标准的 React 协调流程之外。它允许我精确地控制任何特定 API 调用的触发时间。它还允许我在应用程序中的所有其他组件中访问这些 API 的值。
重要的是,我要在应用程序的“顶部”调用它。通过在那里调用它,我确保在应用程序执行期间,无论何时何地,我都可以随时使用我loadSharedHooks()
加载的任何端点。loadSharedHooks()
好奇这个use
物体里面是什么吗?它看起来像这样:
/common/objects/use.js
export const use = {};
就是这样。这就是整个use.js
文件。它只是一个普通的 JavaScript 对象。关键在于,通过在应用程序顶部调用它,我可以在任何地方/任何时间引用里面的值。在本例中,管理我所访问的端点use
的 Hook将被保存到 中。use
/common/hooks/use.reservations.endpoint.js
import { getAxios } from '../functions/get.axios';
import { useState } from 'react';
export const useReservationsEndpoint = () => {
const [reservations, setReservations] = useState([]);
const axios = getAxios();
const loadReservations = async () => {
const response = await axios.call(
'GET',
'https://cove-coding-challenge-api.herokuapp.com/reservations'
);
if (response.status === 200) setReservations(response.data);
};
return {
loadReservations,
reservations,
};
};
这段代码管理我们用于此演示的单个端点。实际的调用在 中处理loadReservations()
。它利用了我自定义的axios
包装器。(我不会axios
在这里概述包装器。如果您愿意,可以在 StackBlitz 演示中仔细查看。如果这是一个“完整”的应用程序,我会在axios
包装器中包含用于POST
、PUT
和PATCH
操作的函数。但对于这个简单的演示,包装器仅包含调用的代码GET
。)
loadReservation
请注意,在此端点 Hook 中,我仅返回和的值reservations
。reservations
包含从端点返回的数据。loadReservations()
允许我们调用GET
操作,而无需在组件主体中编写完整的异步代码。setReservations
不返回。这可以防止下游组件在不使用这个自定义 Hook 的情况下直接尝试更新端点值。
UI.js
import React from 'react';
import { useConstructor } from '@toolz/use-constructor';
import { use } from './common/objects/use';
import { Reservations } from './Reservations';
export const UI = () => {
useConstructor(() => use.reservationsEndpoint.loadReservations());
return <Reservations/>;
};
<UI/>
并没有做太多事情。表面上看,它只是调用了<Reservations/>
。但这里有一个关键特性:它利用了useConstructor()
加载一次(且仅一次)loadReservations()
调用。这确保了我们不会每次应用执行重新渲染时都加载预订端点。一旦完成,它就会直接渲染<Reservations/>
。
Reservations.js
import React, { useState } from 'react';
import { use } from './common/objects/use';
export const Reservations = () => {
const [index, setIndex] = useState(0);
const reservationsEndpoint = use.reservationsEndpoint;
const displayCurrentReservation = () => {
if (reservationsEndpoint.reservations.length === 0)
return null;
const reservation = reservationsEndpoint.reservations[index];
return <>
<br/>
<div>
Room Name: {reservation.room.name}
<br/>
Start Datetime: {reservation.start}
<br/>
End Datetime: {reservation.end}
</div>
<br/>
</>
}
const displayNextButton = () => {
if (reservationsEndpoint.reservations.length === 0 || index === reservationsEndpoint.reservations.length - 1)
return null;
return <>
<button onClick={() => setIndex(index + 1)}>
Next
</button>
</>
}
const displayPreviousButton = () => {
if (reservationsEndpoint.reservations.length === 0 || index === 0)
return null;
return <>
<button
onClick={() => setIndex(index - 1)}
style={{marginRight: 20}}
>
Previous
</button>
</>
}
return <>
<div>
{reservationsEndpoint.reservations.length} reservations found
</div>
<div>
Current showing reservation #{index}:
</div>
{displayCurrentReservation()}
{displayPreviousButton()}
{displayNextButton()}
</>;
}
显然,这是该应用程序的“核心”。以下是它所实现功能的简要概述:
-
它为设置了一个状态变量
index
,因此我们始终知道我们正在查看哪个预订。 -
它访问
reservationsEndpoint
先前已加载的loadSharedHooks()
。 -
然后,它会显示检索到的预订总数、当前预订的索引以及预订本身的一些基本信息。它还会显示
Previous
和Next
按钮,允许您在现有预订中向前或向后循环。
总结
-
如果您在查看 StackBlitz 演示时打开检查器工具,您会发现对
GET
预订端点的 仅被调用一次。即使您使用Previous
或Next
按钮,GET
即使 的状态已更新且组件被反复重新渲染,该调用也不会重复<Reservations/>
。 -
这是在没有使用任何第三方包的情况下完成的。没有 Redux (或 Redux Saga )。没有 Apollo。也没有其他第三方状态管理工具。
-
API 调用从不依赖于 React 协调过程,这意味着我们既不使用基于类的组件固有的生命周期方法,也不使用由此产生的混乱的依赖关系
useEffect()
。 -
我希望您牢记的最重要的一点是,API 调用应该始终受到严格控制。您的应用不应该为了相同的数据而重复调用同一个端点。