避免 React useEffect 中的竞争条件和内存泄漏
让我们学习如何处理“无法对未安装的组件执行 React 状态更新”警告
让我们看一下从 API 请求获取数据的实现,看看此组件中是否存在发生竞争条件的可能性:
import React, { useEffect} from 'react';
export default function UseEffectWithRaceCondition() {
const [todo, setTodo] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const newData = await response.json();
setTodo(newData);
};
fetchData();
}, []);
if (data) {
return <div>{data.title}</div>;
} else {
return null;
}
}
我们指定了一个空数组作为useEffect React hook的依赖项。这样就确保了获取请求只发生一次。但是这个组件仍然容易出现竞争条件和内存泄漏。这是怎么回事?
如果 API 服务器响应时间过长,并且在收到响应之前组件已被卸载,则会发生内存泄漏。尽管组件已被卸载,但请求完成后仍会收到响应。响应随后会被解析并调用 setTodo 方法。React 会抛出以下警告:
无法对已卸载的组件执行 React 状态更新。这是一个无操作,但它表明您的应用程序存在内存泄漏。要修复此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。
其信息非常直接。
同一问题的另一个潜在场景可能是待办事项列表 ID 被作为 prop 传入。
import React, { useEffect} from 'react';
export default function UseEffectWithRaceCondition( {id} ) {
const [todo, setTodo] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const newData = await response.json();
setTodo(newData);
};
fetchData();
}, [id]);
if (data) {
return <div>{data.title}</div>;
} else {
return null;
}
}
如果钩子在请求完成之前收到了不同的 ID,并且第二个请求在第一个请求之前完成,我们将在组件中显示第一个请求的数据。
竞争条件问题的潜在解决方案
有几种方法可以解决这个问题。这两种方法都利用了 useEffect 提供的清理功能。
-
我们可以使用布尔值标志来确保组件已挂载。这样,只有当标志为 true 时,我们才会更新状态。如果我们在组件内部发出多个请求,我们将始终显示最后一个请求的数据。
-
我们可以使用 AbortController 在组件卸载时取消之前的请求。不过 IE 不支持 AbortController。所以如果要使用这种方法,我们需要考虑这一点。
使用布尔标志进行 useEffect 清理
useEffect(() => {
let isComponentMounted = true;
const fetchData = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const newData = await response.json();
if(isComponentMounted) {
setTodo(newData);
}
};
fetchData();
return () => {
isComponentMounted = false;
}
}, []);
此修复依赖于 useEffect 的清理函数的工作方式。如果组件渲染多次,则会在执行下一个 effect 之前清理上一个 effect。
由于这种工作方式,它对于我们另一个由于 ID 被更改而产生的多个请求的示例也能正常工作。我们仍然会遇到竞争条件,因为后台会有多个请求在执行。但只有最后一个请求的结果才会显示在 UI 上。
使用 AbortController 进行 useEffect 清理
虽然之前的方法有效,但它并不是处理竞争条件的最佳方法。请求在后台进行。后台保留过时的请求会不必要地消耗用户的带宽。而且浏览器还会限制最大并发请求数(最多 6-8 个)。
从我们之前关于如何取消 HTTP 获取请求的文章中,我们了解到 DOM 标准中新增的 AbortController API。我们可以利用它来中止我们的请求。
useEffect(() => {
let abortController = new AbortController();
const fetchData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1', {
signal: abortController.signal,
});
const newData = await response.json();
setTodo(newData);
}
catch(error) {
if (error.name === 'AbortError') {
// Handling error thrown by aborting request
}
}
};
fetchData();
return () => {
abortController.abort();
}
}, []);
由于中止请求会引发错误,我们需要明确处理它。
这个解决方案和上一个类似。在重新渲染的情况下,清理函数会在执行下一个 effect 之前执行。不同之处在于,由于我们使用了 AbortController,浏览器也会取消请求。
以上就是我们在使用 React 的 useEffect hook 发起 API 请求时避免竞争条件的两种方法。如果你想使用一些允许取消请求的第三方库,可以使用 Axios 或 ReactQuery,它们也提供了许多其他功能。
如果您有任何疑问,请在下面发表评论。
最初于 2021 年 2 月 8 日发布于https://www.wisdomgeek.com 。
文章来源:https://dev.to/saranshk/avoiding-race-conditions-and-memory-leaks-in-react-useeffect-3mme