我最近需要在 React 中创建一个部件,该部件从多个 API 端点获取数据。当用户点击时,会获取新的数据并将其编组到 UI 中。但这导致了一些问题。
一个问题很快变得显而易见:如果用户点击速度足够快,当先前的网络请求得到解决时,UI 会在短时间内显示不正确、过时的数据。
我们可以 去抖动 我们的 UI 交互,但这从根本上无法解决我们的问题。过时的网络获取将得到解决并使用错误的数据更新我们的 UI,直到最终的网络请求完成并使用最终的正确状态更新我们的 UI。在较慢的连接上,问题变得更加明显。此外,我们还留下了无用的网络请求,浪费了用户的带宽。
我构建了一个示例来说明这个问题。它通过很酷的 Cheap Shark API 使用现代的fetch()
方法从 Steam 获取游戏交易。尝试快速更新价格限制,您将看到 UI 如何闪烁错误数据,直到最终稳定下来。
解决方案
事实证明,有一种方法可以使用AbortController
中止挂起的 DOM 异步请求。您可以使用它来取消 HTTP 请求以及 事件侦听器。
—Mozilla 开发者网络
AbortController
接口表示一个控制器对象,允许您根据需要中止一个或多个 Web 请求。
AbortController
API 非常简单:它公开了一个AbortSignal
,我们将其插入我们的fetch()
调用中,如下所示
const abortController = new AbortController()
const signal = abortController.signal
fetch(url, { signal })
从这里开始,我们可以调用abortController.abort()
以确保我们的挂起获取被中止。
让我们重写我们的示例,以确保我们正在取消任何挂起的获取并将仅从 API 收到的最新数据编组到我们的应用程序中
代码大多相同,但有一些关键区别
- 它在
<App />
组件中的useRef
中创建一个新的缓存变量abortController
。 - 对于每个新的获取,它都会使用新的
AbortController
初始化该获取并获取其对应的AbortSignal
。 - 它将获得的
AbortSignal
传递给fetch()
调用。 - 它在下次获取时自行中止。
const App = () => {
// Same as before, local variable and state declaration
// ...
// Create a new cached variable abortController in a useRef() hook
const abortController = React.useRef()
React.useEffect(() => {
// If there is a pending fetch request with associated AbortController, abort
if (abortController.current) {
abortController.abort()
}
// Assign a new AbortController for the latest fetch to our useRef variable
abortController.current = new AbortController()
const { signal } = abortController.current
// Same as before
fetch(url, { signal }).then(res => {
// Rest of our fetching logic, same as before
})
}, [
abortController,
sortByString,
upperPrice,
lowerPrice,
])
}
结论
就是这样!我们现在兼具两者的优势:我们对 UI 交互进行去抖动,并且手动取消过时的挂起网络获取。这样,我们就可以确保我们的 UI 只更新一次,并且只使用来自我们 API 的最新数据进行更新。
CheapShark 的创建者在这里,感谢您在文章中使用 API!我使用了 CSS Tricks 大约 10 年了,所以看到我的网站出现在 API 用法的示例中真是太棒了! :)
嘿!太酷了!我很高兴您喜欢它,并感谢您提供这个很棒的公共 API!
我认为您在示例中调用
.abort()
时缺少.current
。Refs 在这种情况下有点麻烦。(我想念this
!)嘿!如果您查看第二个演示,在发出
fetch
命令之前,我实际上确实使用了.current
属性,如下所示abortController.current.abort()
如果您还有其他想法,请告诉我?感谢您的反馈!
哦,我的意思是结论之前的最后一个代码示例中。上面的 Codepen 已经没问题了。
很棒的提示……但更进一步,如何从您的
useEffect
钩子中返回一个清理函数,该函数还中止在组件卸载时可能正在进行中的任何 API 调用?也许我很奇怪,但我并不介意响应快速点击的多次更新,我认为这表明它正在响应每次点击并正常工作。
如果它只更新一次,我可能会怀疑它是否只根据第一次点击进行了更新(这是由于与具有较差 UX 的界面的体验导致的)。
问题是响应可能不会按照请求的顺序到达。如果每个响应都覆盖先前的数据,则会导致错误。
一个常见的示例是搜索请求,当用户键入时,这些请求每秒可能会触发多次。如果未取消先前的搜索请求,则该响应可能会在更新的结果之后到达。这会导致一个恼人的错误,即显示错误的搜索结果。
当新的请求已经在进行中时,处理过时的搜索响应也是一种浪费周期。
您不会对已取消的请求感到困惑。它们在“网络”选项卡中被列为正常状态,只是状态为“已取消”。
在这里使用 ref“太多了”。这里不需要。更好的方法是使用 useEffect 的返回函数。
每次您的效果被调用时创建一个新的控制器,并返回() => abortController.abort()
嗨,这是一个完全有效的观点!我没有看到任何缺点。