正确处理 React 组件中的 async/await
语境
最近有推文指出async/await与React组件配合不佳,除非处理方式具有一定程度的复杂性。
为什么这么复杂?
处理异步代码在 React 以及其他大多数 UI 库/框架中都很复杂。原因是,在我们等待异步代码执行完成的任何时候,组件的 props 都可能被更新,或者组件本身也可能被卸载。
暴露问题
正如第一条推文所述,这很复杂,但我会尝试解释这里发生了什么。
在下面的代码片段中,我们将查看使用 axios 库发出异步 HTTP 请求的组件:
import React, { useState, useEffect } from "react";
import axios from "axios";
export default function RandomJoke({ more, loadMore }) {
const [joke, setJoke] = useState("");
useEffect(() => {
async function fetchJoke() {
try {
const asyncResponse = await axios("https://api.icndb.com/jokes/random");
const { value } = asyncResponse.data;
setJoke(value.joke);
} catch (err) {
console.error(err);
}
}
fetchJoke();
}, [more]);
return (
<div>
<h1>Here's a random joke for you</h1>
<h2>{`"${joke}"`}</h2>
<button onClick={loadMore}>More...</button>
</div>
);
}
那么...上述组件有什么问题?
1)如果在异步请求完成之前卸载组件,异步请求仍会运行,并在完成时调用 setState 函数,从而导致 React 警告😕:
2)如果在异步请求完成之前更改了 "more" 属性,则此效果将再次运行,因此异步函数将再次被调用。如果第一个请求在第二个请求之后完成,这可能会导致竞争条件。
这可能是错误的,因为我们想要获得我们请求的最新异步调用的结果。
显然,在这种简单的应用程序中这是可以的,但是假设您有一个根据某些搜索文本查询 API 的应用程序 - 您总是希望显示所输入的最新查询的结果。
如何修复
问题 1 - 使用 ref 修复 React 警告:
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
export default function RandomJoke({ more, loadMore }) {
const [joke, setJoke] = useState("");
const componentIsMounted = useRef(true);
useEffect(() => {
// each useEffect can return a cleanup function
return () => {
componentIsMounted.current = false;
};
}, []); // no extra deps => the cleanup function run this on component unmount
useEffect(() => {
async function fetchJoke() {
try {
const asyncResponse = await axios("https://api.icndb.com/jokes/random");
const { value } = asyncResponse.data;
if (componentIsMounted.current) {
setJoke(value.joke);
}
} catch (err) {
console.error(err);
}
}
fetchJoke();
}, [more]);
return (
<div>
<h1>Here's a random joke for you</h1>
<h2>{`"${joke}"`}</h2>
<button onClick={loadMore}>More...</button>
</div>
);
}
如你所见,我们上面所做的就是添加一个 ref componentIsMounted,它在组件卸载时只会更新。为此,我们添加了一个清理函数来达到额外的效果。然后,在设置状态之前获取数据时,我们会检查组件是否仍然处于挂载状态。问题解决了✅!现在让我们来修复它:
问题 2:修复实际的异步问题。我们想要的是,如果我们请求了一些异步工作,我们需要一种方法来取消它,以防它没有完成,而与此同时有人再次请求它。幸运的是,axios正好有我们需要的东西——一个取消令牌💥
import React, { useState, useEffect, useRef } from "react";
import axios, { CancelToken } from "axios";
export default function RandomJoke({ more, loadMore }) {
const [joke, setJoke] = useState("");
const componentIsMounted = useRef(true);
useEffect(() => {
// each useEffect can return a cleanup function
return () => {
componentIsMounted.current = false;
};
}, []); // no extra deps => the cleanup function run this on component unmount
useEffect(() => {
const cancelTokenSource = CancelToken.source();
async function fetchJoke() {
try {
const asyncResponse = await axios(
"https://api.icndb.com/jokes/random",
{
cancelToken: cancelTokenSource.token,
}
);
const { value } = asyncResponse.data;
if (componentIsMounted.current) {
setJoke(value.joke);
}
} catch (err) {
if (axios.isCancel(err)) {
return console.info(err);
}
console.error(err);
}
}
fetchJoke();
return () => {
// here we cancel preveous http request that did not complete yet
cancelTokenSource.cancel(
"Cancelling previous http call because a new one was made ;-)"
);
};
}, [more]);
return (
<div>
<h1>Here's a random joke for you</h1>
<h2>{`"${joke}"`}</h2>
<button onClick={loadMore}>More...</button>
</div>
);
}
这里发生了什么:
1) 每次调用获取异步数据的 effect 时,我们都会创建一个取消 token 源,并将其传递给 axios。2
) 如果在异步工作完成之前再次调用该 effect,我们会利用 React 的useEffect清理函数。清理过程会在 effect 再次调用之前运行,因此我们可以通过调用cancellationTokenSource.cancel()来执行取消操作。
结论
所以,在 React 中处理异步工作确实有点复杂。当然,我们可以通过使用自定义钩子来抽象它,从而获取数据。
您可能不必总是担心每种情况下的这些问题。如果您的组件隔离良好,这意味着它运行的异步代码不依赖于 props 值,那么事情应该没问题……您可能仍然会不时遇到卸载问题,如果您的组件经常卸载,那么您可能也应该修复这个问题。
正确处理 React 组件中的 async/await - 第 2 部分
文章来源:https://dev.to/alexandrudanpop/ Correctly-handling-async-await-in-react-components-4h74