注意 React.useEffect 竞态条件 🐛 错误
概括
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
ReactuseEffect引入竞态条件错误是很常见的。这种情况可能发生在任何时候,只要你在代码中包含异步代码React.useEffect。
什么是竞态条件错误?
当两个异步进程同时更新同一个值时,就会发生竞态条件。在这种情况下,最终更新该值的是最后一个完成的进程。
这可能并非我们所愿。我们或许希望启动最后一个进程来更新该值。
例如,某个组件会获取数据,然后重新渲染并重新获取数据。
示例竞态条件组件
这是一个可能存在竞态条件缺陷的组件示例。
import { useEffect, useState } from "react";
import { getPerson } from "./api";
export const Race = ({ id }) => {
const [person, setPerson] = useState(null);
useEffect(() => {
setPerson(null);
getPerson(id).then((person) => {
setPerson(person);
};
}, [id]);
return person ? `${id} = ${person.name}` : null;
}
乍一看,这段代码似乎没有任何问题,而这正是这个漏洞如此危险的原因。
useEffect每次id更改时都会触发并调用getPerson。如果getPerson已启动且发生id更改,则会启动第二次调用getPerson。
如果第一次调用在第二次调用之前完成,那么第二次调用会person用第一次调用的数据覆盖第二次调用的数据,从而导致我们的应用程序出现错误。
中止控制器
使用时fetch,您可以使用AbortController手动中止第一个请求。
注意:稍后我们会找到更简单的方法来实现这个功能。这段代码仅用于教学目的。
import { useEffect, useRef, useState } from "react";
import { getPerson } from "./api";
export const Race = ({ id }) => {
const [data, setData] = useState(null);
const abortRef = useRef(null);
useEffect(() => {
setData(null);
if (abortRef.current != null) {
abortRef.current.abort();
}
abortRef.current = new AbortController();
fetch(`/api/${id}`, { signal: abortRef.current.signal })
.then((response) => {
abortRef.current = null;
return response;
})
.then((response) => response.json())
.then(setData);
}, [id]);
return data;
}
取消之前的请求
对于我们来说,这AbortController并非总是可行的,因为有些异步代码无法与异步操作一起使用AbortController。所以我们仍然需要一种方法来取消之前的异步调用。
这可以通过cancelled在内部设置一个标志来实现useEffect。我们可以利用该功能在更改true时设置此标志。idunmountuseEffect
注意:稍后我们会找到更简单的方法来实现这个功能。这段代码仅用于教学目的。
import { useEffect, useState } from "react";
import { getPerson } from "./api";
export const Race = ({ id }) => {
const [person, setPerson] = useState(null);
useEffect(() => {
let cancelled = false;
setPerson(null);
getPerson(id).then((person) => {
if (cancelled) return; // only proceed if NOT cancelled
setPerson(person);
};
return () => {
cancelled = true; // cancel if `id` changes
};
}, [id]);
return person ? `${id} = ${person.name}` : null;
}
使用 React 查询
我不建议在每个组件中手动处理中止或取消操作。相反,你应该将该功能封装在一个 React Hook 中。幸运的是,已经有一个库为我们完成了这项工作。
我建议使用react-query库。这个库可以防止竞态条件错误,还提供了一些其他实用功能,例如缓存、重试等等。
我也很喜欢react-query简化代码的方式。
import { useQuery } from "react-query";
import { getPerson } from "./api";
export const Race = ({ id }) => {
const { isLoading, error, data } = useQuery(
["person", id],
(key, id) => getPerson(id)
);
if (isLoading) return "Loading...";
if (error) return `ERROR: ${error.toString()}`;
return `${id} = ${data.name}`;
}
react-query的第一个参数是缓存键,第二个参数是一个函数,当没有缓存、缓存过期或无效时,该函数将被调用。
概括
当内部存在异步调用React.useEffect并React.useEffect再次触发时,可能会出现竞态条件错误。
使用时fetch,您可以中止请求。APromise可以被取消。但我建议不要为每个组件手动编写该代码,而是使用像react-query这样的库。
请在joel.net上订阅我的新闻简报。
在 Twitter 上关注我@joelnet,或在 YouTube 上关注JoelCodes 。
干杯🍻
文章来源:https://dev.to/joelnet/beware-of-react-useeffect-race-condition-bugs-42hl